diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index e1737825a..aa374676d 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -352,7 +352,7 @@ This assessment was reconciled with the source tree on 2026-08-26. Unless a resu - The single-still `Av1Decoder` path now parses tile state before allocating and reconstructing one independently decodable frame, and it disposes the reconstruction planes after pixel conversion. It deliberately does not retain animation/video reference frames or implement `show_existing_frame` playback state. - Transform coefficient entropy derivation and updates now address the above contexts relative to the tile column and the left contexts relative to the current superblock row, preserve luma coordinates independently of chroma subsampling, and test every packed context entry for the libaom any-nonzero rule. Extended vertical partition updates advance the mode-information column rather than the row. The existing multi-superblock 4:4:4 AVIF fixture now completes tile parsing; independent coefficient-context vectors across tile boundaries, chroma layouts, bit depths, and edge-clipped transforms remain required. -- The reconstruction pipeline now records plane-relative transform geometry, preserves tile-local delta-Q and delta-LF predictors, derives segmentation and reference-adjusted filter levels, and runs the exact AV1 4-, 6-, 8-, and 14-tap deblocking kernels in normative vertical-then-horizontal order. Deblocking uses the same closed edge-operator architecture as the HEVC filter, with operators specialized by sample storage and orientation, `Vector128` lanes representing the four rows or columns along an edge, and an allocation-free scalar fallback for disabled intrinsics. Exact native-plane comparison with pinned scalar libaom output now verifies active deblocking and complete reconstruction for real 8-bit 4:2:0, 10-bit 4:4:4, and 12-bit 4:4:4 content; genuine AVIF containers separately verify presentation and public bit-depth metadata. The pipeline then applies CDEF through one semantic filter architecture: paired AVX2 and single-block `Vector128` direction analysis, closed primary/secondary strength operators, packed 4x4/4x8/8x4/8x8 constrained filtering, byte/16-bit output operators, and an exact allocation-free scalar fallback. Decoder orchestration now owns the immutable plane snapshots and clean direction/variance maps through ImageSharp's memory allocator, widens 8-bit source rows with the same AVX2/128-bit/scalar tiers as libaom, lists each unit's non-skipped blocks in fixed inline storage, analyzes listed blocks in pairs, and writes filtered bytes or 16-bit samples directly to the frame planes. Independently encoded active-CDEF 8-bit 4:2:0 and 10/12-bit 4:4:4 streams now match every visible native sample produced by pinned scalar libaom with restoration disabled; matching active-CDEF AVIF presentation fixtures remain required. Active super-resolution derives the Appendix A bounded coded width and applies the exact 64-phase, 8-tap horizontal filter with aligned reconstruction-edge input, 8/10/12-bit clipping, and the existing cross-platform `Vector128_.MultiplyAddAdjacent` helper. Loop restoration follows super-resolution, preserves the required pre-CDEF deblocked context at internal stripes, and applies decoded Wiener or self-guided units from immutable plane snapshots. The visible still-image path then applies the complete self-contained film-grain parameter set after all in-loop filters. Independent 8-, 10-, and 12-bit vectors exercising super-resolution, restoration, and grain remain required before those stages have external pixel-level verification. +- The reconstruction pipeline now records plane-relative transform geometry, preserves tile-local delta-Q and delta-LF predictors, derives segmentation and reference-adjusted filter levels, and runs the exact AV1 4-, 6-, 8-, and 14-tap deblocking kernels in normative vertical-then-horizontal order. Deblocking uses the same closed edge-operator architecture as the HEVC filter, with operators specialized by sample storage and orientation, `Vector128` lanes representing the four rows or columns along an edge, and an allocation-free scalar fallback for disabled intrinsics. Exact native-plane comparison with pinned scalar libaom output now verifies active deblocking and complete reconstruction for real 8-bit 4:2:0, 10-bit 4:4:4, and 12-bit 4:4:4 content; genuine AVIF containers separately verify presentation and public bit-depth metadata. The pipeline then applies CDEF through one semantic filter architecture: paired AVX2 and single-block `Vector128` direction analysis, closed primary/secondary strength operators, packed 4x4/4x8/8x4/8x8 constrained filtering, byte/16-bit output operators, and an exact allocation-free scalar fallback. Decoder orchestration now owns the immutable plane snapshots and clean direction/variance maps through ImageSharp's memory allocator, widens 8-bit source rows with the same AVX2/128-bit/scalar tiers as libaom, lists each unit's non-skipped blocks in fixed inline storage, analyzes listed blocks in pairs, and writes filtered bytes or 16-bit samples directly to the frame planes. Independently encoded active-CDEF 8-bit 4:2:0 and 10/12-bit 4:4:4 streams match every visible native sample produced by pinned scalar libaom with restoration disabled. Independently encoded AVIF containers at the same three bit depths also match pinned scalar-libavif presentation exactly under normal, 256-bit, 128-bit, and scalar color-conversion dispatch. Active super-resolution derives the Appendix A bounded coded width and applies the exact 64-phase, 8-tap horizontal filter with aligned reconstruction-edge input, 8/10/12-bit clipping, and the existing cross-platform `Vector128_.MultiplyAddAdjacent` helper. Loop restoration follows super-resolution, preserves the required pre-CDEF deblocked context at internal stripes, and applies decoded Wiener or self-guided units from immutable plane snapshots. The visible still-image path then applies the complete self-contained film-grain parameter set after all in-loop filters. Independent 8-, 10-, and 12-bit vectors exercising super-resolution, restoration, and grain remain required before those stages have external pixel-level verification. - Palette mode now reads the normative luma/chroma mode and size CDFs, neighbor color caches, high-bit-depth color syntax, diagonal color-index maps, and clipped-edge padding. Reconstruction retains libaom's exact indexed-palette contract while using repeated eight-entry tables and native lane-local shuffles across runtime-selected `Vector512`, `Vector256`, and `Vector128` paths before an exact scalar fallback. `FeatureTestRunner` verifies palette sizes 2-8, transform widths 4-64, padded rows, and 8/10/12-bit samples. This path is not production-complete until its syntax and reconstruction are verified with an independently encoded palette AVIF fixture. - Non-reduced still-image sequence parsing now consumes decoder-model operating parameters, temporal presentation fields, and OBU-layer-applicable buffer-removal fields only to preserve AV1 bit alignment. The scheduling values are not retained, and no movie, track, timing, playback, or generic ISO BMFF surface has been introduced. Existing focused sequence-header coverage exercises only reduced-still syntax, so an independent non-reduced still AVIF vector remains required. - 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. @@ -505,11 +505,11 @@ Implement and verify in dependency order: - [x] Deblocking loop filter. - [x] Implement allocation-free SIMD-first 4-, 6-, 8-, and 14-tap filtering for vertical and horizontal edges in 8/10/12-bit storage through closed edge operators, with exact scalar fallback and `FeatureTestRunner` parity against an independent definition. - [x] Verify deblocking syntax, filter-level derivation, and boundary traversal with independently encoded 8/10/12-bit AV1 samples and exact scalar-libaom planes; verify presented reconstruction and public precision with genuine AVIF containers at every supported bit depth. -- [ ] CDEF. +- [x] CDEF. - [x] Replace the scalar helper bucket with an allocation-free SIMD-first `Av1CdefFilter`: vector direction analysis, 4x4/4x8/8x4/8x8 constrained filtering, byte/16-bit output operators, and one exact scalar fallback verified through `FeatureTestRunner`. - [x] Move the immutable deblocked-plane snapshot and frame-wide direction/variance maps to allocator-owned decoder memory, preserving luma-first paired direction derivation, fixed-storage skipped-block selection, asymmetric chroma direction conversion, frame-edge sentinels, SIMD source widening, and direct frame-plane output. - [x] Verify CDEF syntax, selected nonzero strengths, unit/block traversal, subsampling, frame edges, and exact native reconstruction with independently encoded active-CDEF AV1 streams across 8/10/12-bit samples and pinned scalar-libaom output. - - [ ] Verify presented reconstruction with independently encoded active-CDEF AVIF containers across 8/10/12-bit samples. + - [x] Verify exact presented reconstruction with independently encoded active-CDEF AVIF containers across 8/10/12-bit samples and pinned scalar-libavif output. `FeatureTestRunner` covers normal dispatch, AVX-512 disabled, AVX disabled, and all hardware intrinsics disabled without a comparer tolerance. - [ ] Super-resolution scaling. - [ ] Loop restoration. - [ ] Frame padding and film-grain synthesis when signaled. diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConversionParameters.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConversionParameters.cs index 378571d94..d722fa275 100644 --- a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConversionParameters.cs +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConversionParameters.cs @@ -52,8 +52,10 @@ internal readonly struct HeifColorConversionParameters this.Kb = kb; this.RedChromaScale = 2F * (1F - kr); this.BlueChromaScale = 2F * (1F - kb); - this.GreenRedChromaScale = 2F * kr * (1F - kr) / kg; - this.GreenBlueChromaScale = 2F * kb * (1F - kb) / kg; + this.GreenRedChromaNumerator = kr * (1F - kr); + this.GreenBlueChromaNumerator = kb * (1F - kb); + this.GreenRedChromaScale = 2F * this.GreenRedChromaNumerator / kg; + this.GreenBlueChromaScale = 2F * this.GreenBlueChromaNumerator / kg; this.MatrixCoefficients = matrixCoefficients; this.IsFullRange = isFullRange; this.TransferCharacteristics = transferCharacteristics; @@ -94,6 +96,16 @@ internal readonly struct HeifColorConversionParameters /// public float BlueChromaScale { get; } + /// + /// Gets the red-difference term in the green-component numerator. + /// + public float GreenRedChromaNumerator { get; } + + /// + /// Gets the blue-difference term in the green-component numerator. + /// + public float GreenBlueChromaNumerator { get; } + /// /// Gets the red-difference subtraction from green. /// diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.CoefficientOperator.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.CoefficientOperator.cs index 1c8ced1e6..e7956d10c 100644 --- a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.CoefficientOperator.cs +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.CoefficientOperator.cs @@ -20,10 +20,12 @@ internal abstract partial class HeifColorConverterBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ConvertToRgb(ref float y, ref float cb, ref float cr, in HeifColorConversionParameters parameters) { - // The resolved coefficients are frame invariants. Keeping the operator in this direct matrix form - // lets every SIMD overload map the same three equations to fused multiply-add instructions. + // Preserve the H.273 operation order. Pre-dividing the two green contributions changes rounding at + // exact output-code boundaries for high-bit-depth images. float r = y + (parameters.RedChromaScale * cr); - float g = y - (parameters.GreenRedChromaScale * cr) - (parameters.GreenBlueChromaScale * cb); + float g = y - ((2F * ((parameters.GreenRedChromaNumerator * cr) + + (parameters.GreenBlueChromaNumerator * cb))) / parameters.Kg); + float b = y + (parameters.BlueChromaScale * cb); y = r; @@ -35,12 +37,11 @@ internal abstract partial class HeifColorConverterBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ConvertToRgb(ref Vector128 y, ref Vector128 cb, ref Vector128 cr, in HeifColorConversionParameters parameters) { - Vector128 r = Vector128.MultiplyAddEstimate(Vector128.Create(parameters.RedChromaScale), cr, y); - Vector128 g = Vector128.MultiplyAddEstimate( - Vector128.Create(-parameters.GreenBlueChromaScale), - cb, - Vector128.MultiplyAddEstimate(Vector128.Create(-parameters.GreenRedChromaScale), cr, y)); - Vector128 b = Vector128.MultiplyAddEstimate(Vector128.Create(parameters.BlueChromaScale), cb, y); + Vector128 r = y + (Vector128.Create(parameters.RedChromaScale) * cr); + Vector128 greenRed = Vector128.Create(parameters.GreenRedChromaNumerator) * cr; + Vector128 greenBlue = Vector128.Create(parameters.GreenBlueChromaNumerator) * cb; + Vector128 g = y - ((Vector128.Create(2F) * (greenRed + greenBlue)) / Vector128.Create(parameters.Kg)); + Vector128 b = y + (Vector128.Create(parameters.BlueChromaScale) * cb); y = r; cb = g; @@ -51,12 +52,11 @@ internal abstract partial class HeifColorConverterBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ConvertToRgb(ref Vector256 y, ref Vector256 cb, ref Vector256 cr, in HeifColorConversionParameters parameters) { - Vector256 r = Vector256.MultiplyAddEstimate(Vector256.Create(parameters.RedChromaScale), cr, y); - Vector256 g = Vector256.MultiplyAddEstimate( - Vector256.Create(-parameters.GreenBlueChromaScale), - cb, - Vector256.MultiplyAddEstimate(Vector256.Create(-parameters.GreenRedChromaScale), cr, y)); - Vector256 b = Vector256.MultiplyAddEstimate(Vector256.Create(parameters.BlueChromaScale), cb, y); + Vector256 r = y + (Vector256.Create(parameters.RedChromaScale) * cr); + Vector256 greenRed = Vector256.Create(parameters.GreenRedChromaNumerator) * cr; + Vector256 greenBlue = Vector256.Create(parameters.GreenBlueChromaNumerator) * cb; + Vector256 g = y - ((Vector256.Create(2F) * (greenRed + greenBlue)) / Vector256.Create(parameters.Kg)); + Vector256 b = y + (Vector256.Create(parameters.BlueChromaScale) * cb); y = r; cb = g; @@ -67,12 +67,11 @@ internal abstract partial class HeifColorConverterBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ConvertToRgb(ref Vector512 y, ref Vector512 cb, ref Vector512 cr, in HeifColorConversionParameters parameters) { - Vector512 r = Vector512.MultiplyAddEstimate(Vector512.Create(parameters.RedChromaScale), cr, y); - Vector512 g = Vector512.MultiplyAddEstimate( - Vector512.Create(-parameters.GreenBlueChromaScale), - cb, - Vector512.MultiplyAddEstimate(Vector512.Create(-parameters.GreenRedChromaScale), cr, y)); - Vector512 b = Vector512.MultiplyAddEstimate(Vector512.Create(parameters.BlueChromaScale), cb, y); + Vector512 r = y + (Vector512.Create(parameters.RedChromaScale) * cr); + Vector512 greenRed = Vector512.Create(parameters.GreenRedChromaNumerator) * cr; + Vector512 greenBlue = Vector512.Create(parameters.GreenBlueChromaNumerator) * cb; + Vector512 g = y - ((Vector512.Create(2F) * (greenRed + greenBlue)) / Vector512.Create(parameters.Kg)); + Vector512 b = y + (Vector512.Create(parameters.BlueChromaScale) * cb); y = r; cb = g; diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.Operator.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.Operator.cs index d2ca7be4b..5906a0e2a 100644 --- a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.Operator.cs +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.Operator.cs @@ -194,11 +194,11 @@ internal abstract partial class HeifColorConverterBase if (Vector512.IsHardwareAccelerated && i <= length - Vector512.Count) { Vector512 bias = Vector512.Create(parameters.LumaBias); - Vector512 inverseScale = Vector512.Create(1F / parameters.LumaScale); + Vector512 scale = Vector512.Create(parameters.LumaScale); int oneVectorFromEnd = length - Vector512.Count; for (; i <= oneVectorFromEnd; i += Vector512.Count) { - Vector512 value = (Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) - bias) * inverseScale; + Vector512 value = (Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) - bias) / scale; Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) = value; Unsafe.As>(ref Unsafe.Add(ref component1Base, i)) = value; Unsafe.As>(ref Unsafe.Add(ref component2Base, i)) = value; @@ -208,11 +208,11 @@ internal abstract partial class HeifColorConverterBase if (Vector256.IsHardwareAccelerated && i <= length - Vector256.Count) { Vector256 bias = Vector256.Create(parameters.LumaBias); - Vector256 inverseScale = Vector256.Create(1F / parameters.LumaScale); + Vector256 scale = Vector256.Create(parameters.LumaScale); int oneVectorFromEnd = length - Vector256.Count; for (; i <= oneVectorFromEnd; i += Vector256.Count) { - Vector256 value = (Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) - bias) * inverseScale; + Vector256 value = (Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) - bias) / scale; Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) = value; Unsafe.As>(ref Unsafe.Add(ref component1Base, i)) = value; Unsafe.As>(ref Unsafe.Add(ref component2Base, i)) = value; @@ -222,11 +222,11 @@ internal abstract partial class HeifColorConverterBase if (Vector128.IsHardwareAccelerated && i <= length - Vector128.Count) { Vector128 bias = Vector128.Create(parameters.LumaBias); - Vector128 inverseScale = Vector128.Create(1F / parameters.LumaScale); + Vector128 scale = Vector128.Create(parameters.LumaScale); int oneVectorFromEnd = length - Vector128.Count; for (; i <= oneVectorFromEnd; i += Vector128.Count) { - Vector128 value = (Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) - bias) * inverseScale; + Vector128 value = (Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) - bias) / scale; Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) = value; Unsafe.As>(ref Unsafe.Add(ref component1Base, i)) = value; Unsafe.As>(ref Unsafe.Add(ref component2Base, i)) = value; @@ -247,23 +247,23 @@ internal abstract partial class HeifColorConverterBase float chromaBias = this.ChromaBias; float chromaScale = this.ChromaScale; - // Descending widths preserve vector execution for the remainder left by a wider register. The range - // expansion is folded into each load so operators receive normalized H.273 components directly. + // Descending widths preserve vector execution for the remainder left by a wider register. Divide by the + // signaled ranges directly because multiplying by rounded reciprocals changes exact output-code boundaries. if (Vector512.IsHardwareAccelerated && i <= length - Vector512.Count) { Vector512 lumaBias = Vector512.Create(parameters.LumaBias); - Vector512 inverseLumaScale = Vector512.Create(1F / parameters.LumaScale); + Vector512 lumaScale = Vector512.Create(parameters.LumaScale); Vector512 chromaBiasVector = Vector512.Create(chromaBias); - Vector512 inverseChromaScale = Vector512.Create(1F / chromaScale); + Vector512 chromaScaleVector = Vector512.Create(chromaScale); int oneVectorFromEnd = length - Vector512.Count; for (; i <= oneVectorFromEnd; i += Vector512.Count) { ref Vector512 c0 = ref Unsafe.As>(ref Unsafe.Add(ref component0Base, i)); ref Vector512 c1 = ref Unsafe.As>(ref Unsafe.Add(ref component1Base, i)); ref Vector512 c2 = ref Unsafe.As>(ref Unsafe.Add(ref component2Base, i)); - c0 = (c0 - lumaBias) * inverseLumaScale; - c1 = (c1 - chromaBiasVector) * inverseChromaScale; - c2 = (c2 - chromaBiasVector) * inverseChromaScale; + c0 = (c0 - lumaBias) / lumaScale; + c1 = (c1 - chromaBiasVector) / chromaScaleVector; + c2 = (c2 - chromaBiasVector) / chromaScaleVector; TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters); } @@ -272,18 +272,18 @@ internal abstract partial class HeifColorConverterBase if (Vector256.IsHardwareAccelerated && i <= length - Vector256.Count) { Vector256 lumaBias = Vector256.Create(parameters.LumaBias); - Vector256 inverseLumaScale = Vector256.Create(1F / parameters.LumaScale); + Vector256 lumaScale = Vector256.Create(parameters.LumaScale); Vector256 chromaBiasVector = Vector256.Create(chromaBias); - Vector256 inverseChromaScale = Vector256.Create(1F / chromaScale); + Vector256 chromaScaleVector = Vector256.Create(chromaScale); int oneVectorFromEnd = length - Vector256.Count; for (; i <= oneVectorFromEnd; i += Vector256.Count) { ref Vector256 c0 = ref Unsafe.As>(ref Unsafe.Add(ref component0Base, i)); ref Vector256 c1 = ref Unsafe.As>(ref Unsafe.Add(ref component1Base, i)); ref Vector256 c2 = ref Unsafe.As>(ref Unsafe.Add(ref component2Base, i)); - c0 = (c0 - lumaBias) * inverseLumaScale; - c1 = (c1 - chromaBiasVector) * inverseChromaScale; - c2 = (c2 - chromaBiasVector) * inverseChromaScale; + c0 = (c0 - lumaBias) / lumaScale; + c1 = (c1 - chromaBiasVector) / chromaScaleVector; + c2 = (c2 - chromaBiasVector) / chromaScaleVector; TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters); } @@ -292,18 +292,18 @@ internal abstract partial class HeifColorConverterBase if (Vector128.IsHardwareAccelerated && i <= length - Vector128.Count) { Vector128 lumaBias = Vector128.Create(parameters.LumaBias); - Vector128 inverseLumaScale = Vector128.Create(1F / parameters.LumaScale); + Vector128 lumaScale = Vector128.Create(parameters.LumaScale); Vector128 chromaBiasVector = Vector128.Create(chromaBias); - Vector128 inverseChromaScale = Vector128.Create(1F / chromaScale); + Vector128 chromaScaleVector = Vector128.Create(chromaScale); int oneVectorFromEnd = length - Vector128.Count; for (; i <= oneVectorFromEnd; i += Vector128.Count) { ref Vector128 c0 = ref Unsafe.As>(ref Unsafe.Add(ref component0Base, i)); ref Vector128 c1 = ref Unsafe.As>(ref Unsafe.Add(ref component1Base, i)); ref Vector128 c2 = ref Unsafe.As>(ref Unsafe.Add(ref component2Base, i)); - c0 = (c0 - lumaBias) * inverseLumaScale; - c1 = (c1 - chromaBiasVector) * inverseChromaScale; - c2 = (c2 - chromaBiasVector) * inverseChromaScale; + c0 = (c0 - lumaBias) / lumaScale; + c1 = (c1 - chromaBiasVector) / chromaScaleVector; + c2 = (c2 - chromaBiasVector) / chromaScaleVector; TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters); } diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs index be734b6eb..a97c7d84c 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; @@ -23,6 +24,12 @@ public class Av1ReconstructionConformanceTests /// private const HwIntrinsics ReconstructionConfigurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic; + /// + /// The hardware configurations covering the available vector widths and the scalar color-conversion fallback. + /// + private const HwIntrinsics PresentationConfigurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + /// /// Verifies deblocking syntax, filter activation, component traversal, and presentation for real eight-, ten-, /// and twelve-bit AV1 and AVIF content. @@ -71,6 +78,14 @@ public class Av1ReconstructionConformanceTests public void DecodeWithActiveCdefMatchesPinnedLibaomReference() => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateActiveCdefFixtures, ReconstructionConfigurations); + /// + /// Verifies exact presented pixels and public metadata for independently encoded eight-, ten-, and twelve-bit + /// active-CDEF AVIF images across the available vector widths and the scalar fallback. + /// + [Fact] + public void DecodeWithActiveCdefMatchesPinnedLibavifPresentation() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidatePresentedFixtures, PresentationConfigurations); + /// /// Validates every active-CDEF fixture under the hardware configuration selected by . /// @@ -101,6 +116,34 @@ public class Av1ReconstructionConformanceTests Av1ColorFormat.Yuv444); } + /// + /// Validates every active-CDEF presentation fixture under the hardware configuration selected by + /// . + /// + private static void ValidatePresentedFixtures() + { + ValidatePresentedFixture( + TestImages.Heif.Av1Cdef8BitAvif, + TestImages.Heif.Av1Cdef8BitPresentationReference, + 768, + 512, + HeifBitDepth.Bit8); + + ValidatePresentedFixture( + TestImages.Heif.Av1Cdef10BitAvif, + TestImages.Heif.Av1Cdef10BitPresentationReference, + 1024, + 428, + HeifBitDepth.Bit10); + + ValidatePresentedFixture( + TestImages.Heif.Av1Cdef12BitAvif, + TestImages.Heif.Av1Cdef12BitPresentationReference, + 1024, + 428, + HeifBitDepth.Bit12); + } + /// /// Validates one elementary-stream sample and its containing AVIF image. /// @@ -239,6 +282,36 @@ public class Av1ReconstructionConformanceTests Assert.Equal(metadataBitDepth, metadata.BitDepth); } + /// + /// Validates the exact public presentation of one independently encoded AVIF image against pinned scalar-libavif output. + /// + /// The complete AVIF container. + /// The eight-bit RGBA output produced by the pinned scalar libavif decoder. + /// The expected displayed width. + /// The expected displayed height. + /// The expected public HEIF sample precision. + private static void ValidatePresentedFixture( + string imagePath, + string referencePath, + int width, + int height, + HeifBitDepth metadataBitDepth) + { + DecoderOptions options = new() { MaxFrames = 1 }; + byte[] imageBytes = TestFile.Create(imagePath).Bytes; + byte[] referenceBytes = TestFile.Create(referencePath).Bytes; + using Image image = Image.Load(options, imageBytes); + using Image reference = Image.Load(referenceBytes); + + Assert.Equal(width, image.Width); + Assert.Equal(height, image.Height); + Assert.Single(image.Frames); + HeifMetadata metadata = image.Metadata.GetHeifMetadata(); + Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod); + Assert.Equal(metadataBitDepth, metadata.BitDepth); + ImageComparer.Exact.VerifySimilarity(reference, image); + } + /// /// Compares every visible native component sample with the independent planar reference. /// diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 9bf424bc7..4287fd07d 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1355,10 +1355,16 @@ public static class TestImages public const string Av1Deblocking12BitReference = "Heif/Av1/Conformance/libaom-cosmos1650-12b-libaom.yuv"; public const string Av1Cdef8BitPayload = "Heif/Av1/Conformance/libaom-cdef-kodim23-8b.bit"; public const string Av1Cdef8BitReference = "Heif/Av1/Conformance/libaom-cdef-kodim23-8b-libaom.yuv"; + public const string Av1Cdef8BitAvif = "Heif/Av1/Conformance/libavif-cdef-kodim23-8b.avif"; + public const string Av1Cdef8BitPresentationReference = "Heif/Av1/Conformance/libavif-cdef-kodim23-8b.png"; public const string Av1Cdef10BitPayload = "Heif/Av1/Conformance/libaom-cdef-cosmos-10b.bit"; public const string Av1Cdef10BitReference = "Heif/Av1/Conformance/libaom-cdef-cosmos-10b-libaom.yuv"; + public const string Av1Cdef10BitAvif = "Heif/Av1/Conformance/libavif-cdef-cosmos-10b.avif"; + public const string Av1Cdef10BitPresentationReference = "Heif/Av1/Conformance/libavif-cdef-cosmos-10b.png"; public const string Av1Cdef12BitPayload = "Heif/Av1/Conformance/libaom-cdef-cosmos-12b.bit"; public const string Av1Cdef12BitReference = "Heif/Av1/Conformance/libaom-cdef-cosmos-12b-libaom.yuv"; + public const string Av1Cdef12BitAvif = "Heif/Av1/Conformance/libavif-cdef-cosmos-12b.avif"; + public const string Av1Cdef12BitPresentationReference = "Heif/Av1/Conformance/libavif-cdef-cosmos-12b.png"; } public static class Ico diff --git a/tests/Images/Input/Heif/Av1/Conformance/README.md b/tests/Images/Input/Heif/Av1/Conformance/README.md index 9b7d9c373..fbe35c0c4 100644 --- a/tests/Images/Input/Heif/Av1/Conformance/README.md +++ b/tests/Images/Input/Heif/Av1/Conformance/README.md @@ -1,6 +1,6 @@ # AV1 reconstruction conformance fixtures -The AVIF files come from `libavif/tests/data` at commit `062e582e8afda88e6baf988fdcf046a801efa0f5`. They retain the licenses recorded in libavif's `tests/data/README.md`: the Kodak image is released for unrestricted use, the Cosmos Laundromat frame uses CC BY 3.0, and the libavif color animation is distributed with the libavif test corpus under its BSD-2-Clause license. +The original AVIF and Y4M source files come from `libavif/tests/data` at commit `062e582e8afda88e6baf988fdcf046a801efa0f5`. Derived fixtures retain the licenses recorded in libavif's `tests/data/README.md`: the Kodak image is released for unrestricted use, the Cosmos Laundromat frame uses CC BY 3.0, and the libavif color animation is distributed with the libavif test corpus under its BSD-2-Clause license. The 8- and 10-bit `.bit` files contain the exact AV1 item payloads from the corresponding AVIF files. Each still file has one item occupying the complete `mdat` payload. The genuine 12-bit libavif sequence is retained for container, presentation, alpha, and metadata coverage, but its first color frame disables deblocking and therefore cannot prove the 12-bit filter path. @@ -12,6 +12,10 @@ The `libaom-cdef-*` elementary streams were encoded separately with the same pin The material encoder options were `--usage=2 --passes=1 --limit=1 --obu --end-usage=q --cq-level=30 --cpu-used=4 --threads=1 --lag-in-frames=0 --full-still-picture-hdr --enable-cdef=1 --enable-restoration=0`. Each command also supplied the matching `--bit-depth`, `--input-bit-depth`, and `--profile` values. The 12-bit stream promotes the 10-bit 4:4:4 input through libaom's native 12-bit pipeline. Loop restoration is explicitly disabled so exact output equality exercises deblocking followed by active CDEF without a later restoration stage changing those samples. +The `libavif-cdef-*` AVIF files were independently encoded with `avifenc` 1.4.2 from libavif commit `062e582e8afda88e6baf988fdcf046a801efa0f5` and its pinned libaom 3.14.1 dependency. The material options were `-j 1 -s 4 -q 60`, `enable-cdef=1`, and `enable-restoration=0`. The 8-bit 4:2:0 file uses CICP 1/13/6 and the Kodak Y4M source. The 10-bit 4:4:4 file uses CICP 12/16/12 and the Cosmos Laundromat Y4M source. The 12-bit 4:4:4 input wraps the pinned 12-bit scalar-libaom reference planes as `C444p12` Y4M and also uses CICP 12/16/12. + +The matching `.png` files were produced by `avifdec` from the same scalar build with `-j 1 -d 8`; the 8-bit 4:2:0 reference additionally selected bilinear chroma upsampling. The build uses `AOM_TARGET_CPU=generic` and `AVIF_LIBYUV=OFF`, so both AV1 reconstruction and YUV-to-RGB presentation come from the pinned scalar libaom/libavif paths. ImageSharp compares every presented RGBA byte exactly, without a tolerance. + The native reference layouts are: - `libavif-kodim23-8b-libaom.yuv`: 768x512, 8-bit YUV 4:2:0, planar Y/U/V. diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-cosmos-10b.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-cosmos-10b.avif new file mode 100644 index 000000000..30d705227 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-cosmos-10b.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4046de65fd6534fef9b82bec5ca188268c9b8a969c7a6ff3699b45e4a555b2b2 +size 35092 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-cosmos-10b.png b/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-cosmos-10b.png new file mode 100644 index 000000000..4a018c986 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-cosmos-10b.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a70c1dfd57fb1464719c2c0140a99503e7818c50036acf3d152a7d71ff5a03e +size 473983 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-cosmos-12b.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-cosmos-12b.avif new file mode 100644 index 000000000..331c565fd --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-cosmos-12b.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46bc4f5d6a199e085c5a0e17a9b6f92cc7360115fa2a877ca5e8a623dd8aa546 +size 26258 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-cosmos-12b.png b/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-cosmos-12b.png new file mode 100644 index 000000000..e944926ab --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-cosmos-12b.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19a4e29fa8df389dc8d5120a6278036154e412ea2ca86fef83eb7ef0efa0a27a +size 420742 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-kodim23-8b.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-kodim23-8b.avif new file mode 100644 index 000000000..47f9f07f0 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-kodim23-8b.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9149704cf62a07bb8375066149b3d588d4bf3f8c335355de4f6dce4236acfb5b +size 22882 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-kodim23-8b.png b/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-kodim23-8b.png new file mode 100644 index 000000000..7ab2e6b20 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-kodim23-8b.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19f3e0d5357df5dacf16dc73501e5bbe280f04e0f0e0831bfc8f2511497dccf4 +size 408944