From cf0c81eb3cc64656ec0127c7f71491f0b9f48f55 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 27 Aug 2026 11:07:41 +1000 Subject: [PATCH] Complete AV1 intra-block copy decoding --- HEIF_IMPLEMENTATION_PLAN.md | 7 +- .../Av1/Entropy/Av1DefaultDistributions.cs | 21 + .../Entropy/Av1DisplacementVectorContext.cs | 176 +++++++ .../Heif/Av1/Entropy/Av1Distribution.cs | 19 + .../Av1/Entropy/Av1SymbolContextHelper.cs | 182 +++++-- .../Heif/Av1/Entropy/Av1SymbolDecoder.cs | 102 +++- .../Heif/Av1/Entropy/Av1SymbolEncoder.cs | 16 +- .../Heif/Av1/Motion/Av1IntraBlockCopy.cs | 451 ++++++++++++++++++ .../Heif/Av1/Motion/Av1MotionVector.cs | 74 +++ .../Av1IntraBlockCopyPredictor.Arithmetic.cs | 80 ++++ .../Av1IntraBlockCopyPredictor.Operator.cs | 378 +++++++++++++++ ...ntraBlockCopyPredictor.OperatorContract.cs | 136 ++++++ .../Av1IntraBlockCopyPredictor.Operators.cs | 279 +++++++++++ .../Av1IntraBlockCopyPredictor.cs | 231 +++++++++ .../Heif/Av1/Tiling/Av1BlockModeInfo.cs | 8 +- .../Heif/Av1/Tiling/Av1PartitionInfo.cs | 29 ++ .../Formats/Heif/Av1/Tiling/Av1TileReader.cs | 62 ++- .../Heif/Av1/Transform/Av1BlockDecoder.cs | 117 ++++- .../Heif/Av1/Av1CoefficientsEntropyTests.cs | 15 +- .../Formats/Heif/Av1/Av1EntropyTests.cs | 47 +- .../Av1/Av1IntraBlockCopyPredictorTests.cs | 235 +++++++++ .../Heif/Av1/Av1IntraBlockCopyTests.cs | 149 ++++++ .../Formats/Heif/Av1/Av1SymbolContextTests.cs | 4 +- 23 files changed, 2717 insertions(+), 101 deletions(-) create mode 100644 src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DisplacementVectorContext.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopy.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVector.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.Arithmetic.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.Operator.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.OperatorContract.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.Operators.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.cs create mode 100644 tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyPredictorTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyTests.cs diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 9cad6257e..1e005cd8a 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -29,7 +29,7 @@ Checkboxes may be marked complete only when the implementation and the verificat ## Delivery dashboard -Last reconciled with the source tree on 2026-08-27 against the worktree based on commit `20df9115d`, including the completed AV1 transform architecture checkpoint. This dashboard is the authoritative delivery order. The detailed phase checklists below provide subsystem evidence; they do not override the current-stage marker or permit work to skip ahead. +Last reconciled with the source tree on 2026-08-27 against the worktree based on commit `5a37cfb1a`, including the completed AV1 transform and OBU-framing checkpoints and the current intra-block-copy implementation. This dashboard is the authoritative delivery order. The detailed phase checklists below provide subsystem evidence; they do not override the current-stage marker or permit work to skip ahead. Status meanings: @@ -46,7 +46,7 @@ Immediate checkpoint: **remove every remaining valid AV1 still-image unsupported | --- | --- | --- | --- | --- | | 1 | Baseline, provenance, documentation, and public contract | In progress | Pinned codec references, a bounded image-only scope, encoder options, typed bit depth, decoder-option propagation, and extensive HEIF documentation exist. | Complete the all-file documentation audit, record a fresh Release baseline, finish distinct public HEIC/AVIF save boundaries, and close API review. | | 2 | Bounded HEIF item and image-sequence container | In progress | Still-item parsing, grids, auxiliary alpha, metadata properties, bounded image-sequence tracks, Identify, and all-sync AV1 sequence presentation are connected. | Complete adversarial boundary coverage, remaining item/property behavior, reference-dependent sequence reconstruction, and the bounded sequence writer. | -| 3 | Still-image AV1 and HEVC decoding | **Current** | HEVC reconstruction reaches exact HM/libheif fixtures across the recorded 8/10/12-bit and chroma cases. AV1 includes reconstruction, filter, grain, color, and SIMD-first transforms using one static-generic stage architecture across the supported vector widths. | Remove every valid AV1 still-image unsupported branch with independent vectors, followed by the remaining HEVC profile and Range Extensions conformance matrix. | +| 3 | Still-image AV1 and HEVC decoding | **Current** | HEVC reconstruction reaches exact HM/libheif fixtures across the recorded 8/10/12-bit and chroma cases. AV1 includes bounded OBU framing, reconstruction, filters, grain, color, transforms, and intra-block-copy syntax and prediction through SIMD-first static-generic operators. | Add independent intra-block-copy AV1/AVIF fixtures, remove every other valid AV1 still-image unsupported branch with independent vectors, then complete the remaining HEVC profile and Range Extensions matrix. | | 4 | Complete decoded presentation and animation | In progress | Shared SIMD-first AV1/HEVC color conversion, ICC application, grids, transforms, direct planar alpha composition, frame metadata, repetition, and independently decodable AV1 sequence samples exist. | Close the full color/ICC cross-product, HEVC sequence decoding, AV1/HEVC reference-dependent samples, frame-local metadata/alpha behavior, and independent animated decode vectors. | | 5 | AV1/AVIF encoding | Not started | RGB-to-planar conversion, forward transforms, OBU writer foundations, options, and container-writing infrastructure exist. | `HeifEncoderCore` still rejects AV1. Implement a real independently decodable lossy/lossless AV1 payload and the complete AVIF item/metadata matrix. | | 6 | HEVC/HEIC encoding | Not started | Shared input color conversion, options, and HEIF writer infrastructure exist. | `HeifEncoderCore` still rejects HEVC. Implement a real independently decodable lossy/lossless HEVC payload and the complete HEIC item/metadata matrix. | @@ -504,6 +504,9 @@ Implement and verify in dependency order: - [x] Implement SIMD-first chroma-from-luma storage, 4:4:4/4:2:2/4:2:0 subsampling, rounded mean subtraction, and 8/10/12-bit prediction with exact scalar fallback and `FeatureTestRunner` parity. - [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. - [x] Verify palette mode syntax and presented reconstruction with an independently encoded palette AVIF fixture. +- [ ] Intra-block copy for still-image intra frames. + - [x] Decode tile-adaptive integer displacement vectors, derive and validate spatial references, apply the inter transform sets, and reconstruct luma and subsampled chroma through allocation-free `Vector512`/`Vector256`/`Vector128` operators with exact-width stores and scalar fallback. `FeatureTestRunner` verifies every transform width, bit-depth storage path, chroma phase, intrinsic tier, scalar continuation, and destination-padding boundary. + - [ ] Verify displacement-vector entropy, spatial candidate ordering, wavefront legality, native 8/10/12-bit reconstruction, and presented output against independently encoded opaque intra-block-copy AV1 and AVIF fixtures from the pinned libaom/libavif reference. - [ ] 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. - [x] Verify lossless syntax, inverse quantization, prediction, and presented reconstruction with independently encoded 8/10/12-bit AVIF fixtures. The tests require coded residuals with palette and intra-block copy disabled, compare every native YUV sample with the pinned generic libaom-backed decoder, and compare every presented RGBA byte with pinned generic libavif exactly under normal hardware dispatch and the scalar fallback. diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs index 202de94bb..800bda9e0 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs @@ -2194,6 +2194,27 @@ internal static class Av1DefaultDistributions new(14738, 21678, 25779, 27901, 29024, 30302, 30980, 31843, 32144, 32413, 32520, 32594, 32622, 32656, 32660) ]; + /// + /// Gets the inter transform-type distributions indexed by transform set and square transform-size group. + /// + public static Av1Distribution[][] InterExtendedTransform => + [ + [new(0), new(0), new(0), new(0)], + [ + new(4458, 5560, 7695, 9709, 13330, 14789, 17537, 20266, 21504, 22848, 23934, 25474, 27727, 28915, 30631), + new(1645, 2573, 4778, 5711, 7807, 8622, 10522, 15357, 17674, 20408, 22517, 25010, 27116, 28856, 30749), + new(2048, 4096, 6144, 8192, 10240, 12288, 14336, 16384, 18432, 20480, 22528, 24576, 26624, 28672, 30720), + new(2048, 4096, 6144, 8192, 10240, 12288, 14336, 16384, 18432, 20480, 22528, 24576, 26624, 28672, 30720) + ], + [ + new(2731, 5461, 8192, 10923, 13653, 16384, 19115, 21845, 24576, 27307, 30037), + new(2731, 5461, 8192, 10923, 13653, 16384, 19115, 21845, 24576, 27307, 30037), + new(770, 2421, 5225, 12907, 15819, 18927, 21561, 24089, 26595, 28526, 30529), + new(2731, 5461, 8192, 10923, 13653, 16384, 19115, 21845, 24576, 27307, 30037) + ], + [new(16384), new(4167), new(1998), new(748)] + ]; + /// /// Gets the intra transform-type distributions indexed by transform set, transform-size group, and intra prediction mode. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DisplacementVectorContext.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DisplacementVectorContext.cs new file mode 100644 index 000000000..6fbc64857 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DisplacementVectorContext.cs @@ -0,0 +1,176 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; + +/// +/// Decodes integer intra-block-copy displacement vectors with tile-adaptive AV1 distributions. +/// +internal sealed class Av1DisplacementVectorContext +{ + /// + /// The number of magnitude classes defined by AV1. + /// + private const int MagnitudeClassCount = 11; + + /// + /// The number of class-zero integer magnitude bits. + /// + private const int ClassZeroBitCount = 1; + + /// + /// The tile-adaptive distribution selecting which vector components are nonzero. + /// + private readonly Av1Distribution joint = new(4096, 11264, 19328); + + /// + /// The tile-adaptive vertical component distributions. + /// + private readonly Component vertical = new(); + + /// + /// The tile-adaptive horizontal component distributions. + /// + private readonly Component horizontal = new(); + + /// + /// Reads an integer displacement vector relative to a spatially derived reference. + /// + /// The tile range decoder. + /// The reference displacement vector. + /// The decoded displacement vector in one-eighth-sample units. + public Av1MotionVector Read(ref Av1SymbolReader reader, Av1MotionVector reference) + { + int jointType = reader.ReadSymbol(this.joint); + + // Joint values 1 and 3 carry a horizontal delta; values 2 and 3 carry a vertical delta. Intra-block copy + // fixes precision to whole luma samples, so the component reader consumes no fractional or high-precision CDFs. + int row = jointType >= 2 ? this.vertical.Read(ref reader) : 0; + int column = (jointType & 1) != 0 ? this.horizontal.Read(ref reader) : 0; + return reference + new Av1MotionVector(row, column); + } + + /// + /// Writes an integer displacement vector relative to a spatially derived reference. + /// + /// The tile range encoder. + /// The displacement vector to encode. + /// The spatially derived reference vector. + public void Write(Av1SymbolWriter writer, Av1MotionVector value, Av1MotionVector reference) + { + int row = value.Row - reference.Row; + int column = value.Column - reference.Column; + int jointType = (row != 0 ? 2 : 0) | (column != 0 ? 1 : 0); + writer.WriteSymbol(jointType, this.joint); + + if (row != 0) + { + this.vertical.Write(writer, row); + } + + if (column != 0) + { + this.horizontal.Write(writer, column); + } + } + + /// + /// Stores the adaptive magnitude distributions for one displacement-vector component. + /// + private sealed class Component + { + /// + /// The distribution selecting the signed magnitude class. + /// + private readonly Av1Distribution magnitudeClass = new(28672, 30976, 31858, 32320, 32551, 32656, 32740, 32757, 32762, 32767); + + /// + /// The distribution selecting the sign of a nonzero component. + /// + private readonly Av1Distribution sign = new(16384); + + /// + /// The distribution selecting either of the two class-zero integer magnitudes. + /// + private readonly Av1Distribution classZero = new(27648); + + /// + /// The binary distributions that reconstruct larger magnitude offsets from least to most significant bit. + /// + private readonly Av1Distribution[] offsetBits = + [ + new(17408), new(17920), new(18944), new(20480), new(22528), + new(24576), new(28672), new(29952), new(29952), new(30720) + ]; + + /// + /// Reads one signed integer-precision component. + /// + /// The tile range decoder. + /// The signed component in one-eighth-sample units. + public int Read(ref Av1SymbolReader reader) + { + bool isNegative = reader.ReadSymbol(this.sign) != 0; + int magnitudeClass = reader.ReadSymbol(this.magnitudeClass); + int offset; + int magnitudeBase; + + if (magnitudeClass == 0) + { + offset = reader.ReadSymbol(this.classZero); + magnitudeBase = 0; + } + else + { + int bitCount = magnitudeClass + ClassZeroBitCount - 1; + offset = 0; + for (int bit = 0; bit < bitCount; bit++) + { + // AV1 transmits the integer offset least-significant bit first, with an independently adapting + // distribution for every bit position. + offset |= reader.ReadSymbol(this.offsetBits[bit]) << bit; + } + + magnitudeBase = (1 << ClassZeroBitCount) << (magnitudeClass + 2); + } + + // Integer precision substitutes the normative fractional values fr=3 and hp=1. The low three bits are + // consequently all one, and the final increment converts the zero-based magnitude representation. + int magnitude = magnitudeBase + (offset << 3) + 8; + return isNegative ? -magnitude : magnitude; + } + + /// + /// Writes one signed integer-precision component. + /// + /// The tile range encoder. + /// The nonzero component in one-eighth-sample units. + public void Write(Av1SymbolWriter writer, int value) + { + int magnitude = Math.Abs(value); + DebugGuard.IsTrue(magnitude > 0 && (magnitude & 7) == 0, "Displacement-vector components must use whole-sample precision."); + + int magnitudeClass = magnitude <= 16 ? 0 : Av1Math.MostSignificantBit((uint)(magnitude - 1)) - 3; + DebugGuard.MustBeLessThan(magnitudeClass, MagnitudeClassCount, nameof(magnitudeClass)); + writer.WriteSymbol(value < 0, this.sign); + writer.WriteSymbol(magnitudeClass, this.magnitudeClass); + + if (magnitudeClass == 0) + { + writer.WriteSymbol((magnitude >> 3) - 1, this.classZero); + return; + } + + int magnitudeBase = 8 << magnitudeClass; + int offset = (magnitude - magnitudeBase - 8) >> 3; + for (int bit = 0; bit < magnitudeClass; bit++) + { + // The decoder reconstructs offsets least-significant bit first, so each adaptive bit model must be + // updated in the same order during encoding. + writer.WriteSymbol(((offset >> bit) & 1) != 0, this.offsetBits[bit]); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs index 278e2bd20..a2feca792 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs @@ -183,6 +183,25 @@ internal class Av1Distribution { } + /// + /// Initializes a new instance of the class for a twelve-symbol alphabet. + /// + /// The cumulative threshold following symbol zero. + /// The cumulative threshold following symbol one. + /// The cumulative threshold following symbol two. + /// The cumulative threshold following symbol three. + /// The cumulative threshold following symbol four. + /// The cumulative threshold following symbol five. + /// The cumulative threshold following symbol six. + /// The cumulative threshold following symbol seven. + /// The cumulative threshold following symbol eight. + /// The cumulative threshold following symbol nine. + /// The cumulative threshold following symbol ten. + public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8, uint p9, uint p10) + : this([p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, 0], 2) + { + } + /// /// Initializes a new instance of the class for a thirteen-symbol alphabet. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolContextHelper.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolContextHelper.cs index fa4cb39d3..3af2b11a0 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolContextHelper.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolContextHelper.cs @@ -13,60 +13,103 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; internal static class Av1SymbolContextHelper { /// - /// Maps clipped top and left coefficient-level classes to the transform-block skip context. + /// The number of transform types represented by each flattened transform-set row. /// - private static ReadOnlySpan TransformBlockSkipContexts => + private const int TransformTypeCount = 16; + + /// + /// The number of AV1 transform sets. + /// + private const int TransformSetCount = 6; + + /// + /// Gets the mapping from each transform set and transform type to its coded symbol index. + /// + private static ReadOnlySpan ExtendedTransformIndices => [ - 1, 2, 2, 2, 3, - 2, 4, 4, 4, 5, - 2, 4, 4, 4, 5, - 2, 4, 4, 4, 5, - 3, 5, 5, 5, 6 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // DCT only + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Inter set 3 + 1, 3, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Intra set 2 + 1, 5, 6, 4, 0, 0, 0, 0, 0, 0, 2, 3, 0, 0, 0, 0, // Intra set 1 + 3, 4, 5, 8, 6, 7, 9, 10, 11, 0, 1, 2, 0, 0, 0, 0, // Inter set 2 + 7, 8, 9, 12, 10, 11, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6 // All 16, inter set 1 ]; /// - /// Maps each transform set and transform type to its coded symbol index. + /// Gets the mapping from transform-set types to their intra and inter transform-type distribution indices. /// - public static readonly int[][] ExtendedTransformIndices = [ - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // DCT only - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // Inter set 3 - [1, 3, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // Intra set 2 - [1, 5, 6, 4, 0, 0, 0, 0, 0, 0, 2, 3, 0, 0, 0, 0], // Intra set 1 - [3, 4, 5, 8, 6, 7, 9, 10, 11, 0, 1, 2, 0, 0, 0, 0], // Inter set 2 - [7, 8, 9, 12, 10, 11, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6], // All 16, inter set 1 + private static ReadOnlySpan ExtendedTransformSetToIndex => + [ + 0, -1, 2, 1, -1, -1, + 0, 3, -1, -1, 2, 1 ]; /// - /// Maps intra transform-set types to their transform-type distribution indices. + /// Gets the mapping from coded transform-type symbols to transform types for each transform set. /// - private static readonly int[] ExtendedTransformSetToIndex = [0, -1, 2, 1, -1, -1]; + private static ReadOnlySpan ExtendedTransformTypes => + [ + + // DCT only. Unused positions retain DCT-DCT so each set occupies one fixed 16-entry row. + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + + // Inter set 3. + Av1TransformType.Identity, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + + // Intra set 2. + Av1TransformType.Identity, Av1TransformType.DctDct, Av1TransformType.AdstAdst, Av1TransformType.AdstDct, + Av1TransformType.DctAdst, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + + // Intra set 1. + Av1TransformType.Identity, Av1TransformType.DctDct, Av1TransformType.VerticalDct, Av1TransformType.HorizontalDct, + Av1TransformType.AdstAdst, Av1TransformType.AdstDct, Av1TransformType.DctAdst, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + + // Inter set 2. + Av1TransformType.Identity, Av1TransformType.VerticalDct, Av1TransformType.HorizontalDct, Av1TransformType.DctDct, + Av1TransformType.AdstDct, Av1TransformType.DctAdst, Av1TransformType.FlipAdstDct, Av1TransformType.DctFlipAdst, + Av1TransformType.AdstAdst, Av1TransformType.FlipAdstFlipAdst, Av1TransformType.AdstFlipAdst, Av1TransformType.FlipAdstAdst, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + + // All 16, inter set 1. + Av1TransformType.Identity, Av1TransformType.VerticalDct, Av1TransformType.HorizontalDct, Av1TransformType.VerticalAdst, + Av1TransformType.HorizontalAdst, Av1TransformType.VerticalFlipAdst, Av1TransformType.HorizontalFlipAdst, Av1TransformType.DctDct, + Av1TransformType.AdstDct, Av1TransformType.DctAdst, Av1TransformType.FlipAdstDct, Av1TransformType.DctFlipAdst, + Av1TransformType.AdstAdst, Av1TransformType.FlipAdstFlipAdst, Av1TransformType.AdstFlipAdst, Av1TransformType.FlipAdstAdst + ]; /// - /// Maps a coded transform-type symbol back to its transform type for each intra transform set. + /// Gets the number of coded symbols in each transform set. /// - public static readonly Av1TransformType[][] ExtendedTransformInverse = [ - [Av1TransformType.DctDct], // DCT only - [], // Inter set 3 - [Av1TransformType.Identity, Av1TransformType.DctDct, Av1TransformType.AdstAdst, Av1TransformType.AdstDct, Av1TransformType.DctAdst], // Intra set 2 - [Av1TransformType.Identity, Av1TransformType.DctDct, Av1TransformType.VerticalDct, Av1TransformType.HorizontalDct, Av1TransformType.AdstAdst, Av1TransformType.AdstDct, Av1TransformType.DctAdst], // Intra set 1 - [], // Inter set 2 - [], // All 16, inter set 1 + private static ReadOnlySpan ExtendedTransformTypeCounts => + [ + 1, 2, 5, 7, 12, 16 ]; /// - /// Defines the number of extra offset bits associated with each end-of-block token. + /// Gets the number of extra offset bits associated with each end-of-block token. /// - public static readonly int[] EndOfBlockOffsetBits = [0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + public static ReadOnlySpan EndOfBlockOffsetBits => [0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; /// - /// Defines the first coefficient position represented by each end-of-block token. + /// Gets the first coefficient position represented by each end-of-block token. /// - public static readonly int[] EndOfBlockGroupStart = [0, 1, 2, 3, 5, 9, 17, 33, 65, 129, 257, 513]; + public static ReadOnlySpan EndOfBlockGroupStart => [0, 1, 2, 3, 5, 9, 17, 33, 65, 129, 257, 513]; /// - /// Maps end-of-block positions below 33 directly to their token. + /// Gets the mapping from end-of-block positions below 33 directly to their token. /// - private static readonly byte[] EndOfBlockToPositionSmall = [ + private static ReadOnlySpan EndOfBlockToPositionSmall => + [ 0, 1, 2, // 0-2 3, 3, // 3-4 4, 4, 4, 4, // 5-8 @@ -75,9 +118,10 @@ internal static class Av1SymbolContextHelper ]; /// - /// Maps groups of 32 larger end-of-block positions to their token. + /// Gets the mapping from groups of 32 larger end-of-block positions to their token. /// - private static readonly byte[] EndOfBlockToPositionLarge = [ + private static ReadOnlySpan EndOfBlockToPositionLarge => + [ 6, // place holder 7, // 33-64 8, @@ -97,6 +141,18 @@ internal static class Av1SymbolContextHelper 11 // 513- ]; + /// + /// Gets the mapping from clipped top and left coefficient-level classes to the transform-block skip context. + /// + private static ReadOnlySpan TransformBlockSkipContexts => + [ + 1, 2, 2, 2, 3, + 2, 4, 4, 4, 5, + 2, 4, 4, 4, 5, + 2, 4, 4, 4, 5, + 3, 5, 5, 5, 6 + ]; + /// /// Reduces a rectangular transform size to the square context used by transform-size distributions. /// @@ -336,21 +392,43 @@ internal static class Av1SymbolContextHelper /// Indicates whether the frame restricts transform choices. /// The permitted transform set. internal static Av1TransformSetType GetExtendedTransformSetType(Av1TransformSize transformSize, bool useReducedSet) + => GetExtendedTransformSetType(transformSize, false, useReducedSet); + + /// + /// Selects the transform set permitted for a transform size, prediction class, and reduced-set flag. + /// + /// The coded transform size. + /// Indicates whether the block uses inter prediction. + /// Indicates whether the frame restricts transform choices. + /// The permitted transform set. + internal static Av1TransformSetType GetExtendedTransformSetType(Av1TransformSize transformSize, bool isInter, bool useReducedSet) { Av1TransformSize squareUpSize = transformSize.GetSquareUpSize(); - if (squareUpSize >= Av1TransformSize.Size32x32) + if (squareUpSize > Av1TransformSize.Size32x32) { return Av1TransformSetType.DctOnly; } + if (squareUpSize == Av1TransformSize.Size32x32) + { + return isInter ? Av1TransformSetType.InterSet3 : Av1TransformSetType.DctOnly; + } + if (useReducedSet) { - return Av1TransformSetType.IntraSet2; + return isInter ? Av1TransformSetType.InterSet3 : Av1TransformSetType.IntraSet2; } Av1TransformSize squareSize = transformSize.GetSquareSize(); - return squareSize == Av1TransformSize.Size16x16 ? Av1TransformSetType.IntraSet2 : Av1TransformSetType.IntraSet1; + if (isInter) + { + return squareSize == Av1TransformSize.Size16x16 ? Av1TransformSetType.InterSet2 : Av1TransformSetType.InterSet1; + } + + return squareSize == Av1TransformSize.Size16x16 + ? Av1TransformSetType.IntraSet2 + : Av1TransformSetType.IntraSet1; } /// @@ -418,19 +496,47 @@ internal static class Av1SymbolContextHelper } } + /// + /// Gets the coded symbol index for a transform type in a transform set. + /// + /// The transform set. + /// The transform type. + /// The coded symbol index. + public static int GetExtendedTransformIndex(Av1TransformSetType setType, Av1TransformType transformType) + => ExtendedTransformIndices[((int)setType * TransformTypeCount) + (int)transformType]; + + /// + /// Gets the transform type represented by a coded symbol in a transform set. + /// + /// The transform set. + /// The coded symbol index. + /// The represented transform type. + public static Av1TransformType GetExtendedTransformType(Av1TransformSetType setType, int symbol) + => ExtendedTransformTypes[((int)setType * TransformTypeCount) + symbol]; + /// /// Gets the number of transform types in a transform set. /// /// The transform set. /// The number of permitted transform types. - internal static int GetExtendedTransformTypeCount(Av1TransformSetType setType) => ExtendedTransformInverse[(int)setType].Length; + internal static int GetExtendedTransformTypeCount(Av1TransformSetType setType) => ExtendedTransformTypeCounts[(int)setType]; /// /// Gets the entropy-distribution index for an intra transform set. /// /// The transform set. /// The distribution index, or -1 for an inter-only set. - internal static int GetExtendedTransformSet(Av1TransformSetType setType) => ExtendedTransformSetToIndex[(int)setType]; + internal static int GetExtendedTransformSet(Av1TransformSetType setType) + => GetExtendedTransformSet(setType, false); + + /// + /// Gets the entropy-distribution index for a transform set and prediction class. + /// + /// The transform set. + /// Indicates whether the block uses inter prediction. + /// The distribution index, or -1 when the set is unavailable for the prediction class. + internal static int GetExtendedTransformSet(Av1TransformSetType setType, bool isInter) + => ExtendedTransformSetToIndex[((isInter ? 1 : 0) * TransformSetCount) + (int)setType]; /// /// Packs the sign of the DC coefficient into a cumulative-level context value. diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs index f462cd660..8aa979c56 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; @@ -23,6 +24,11 @@ internal ref struct Av1SymbolDecoder /// private readonly Av1Distribution tileIntraBlockCopy; + /// + /// The tile-adaptive integer displacement-vector context used by intra-block copy. + /// + private readonly Av1DisplacementVectorContext displacementVector; + /// /// The tile-adaptive switchable loop-restoration distribution. /// @@ -178,6 +184,11 @@ internal ref struct Av1SymbolDecoder /// private readonly Av1Distribution[][][] intraExtendedTransform; + /// + /// The tile-adaptive inter transform-type distributions. + /// + private readonly Av1Distribution[][] interExtendedTransform; + /// /// The configuration providing temporary coefficient-context memory. /// @@ -188,11 +199,6 @@ internal ref struct Av1SymbolDecoder /// private Av1SymbolReader reader; - /// - /// The frame base quantizer used to select coefficient probability models. - /// - private readonly int baseQIndex; - /// /// Initializes a new instance of the struct for one AV1 tile. /// @@ -205,6 +211,7 @@ internal ref struct Av1SymbolDecoder // Every tile starts from its own frame-context copy. Sharing these objects would let one image's adaptive // updates change the initial probabilities used to decode the next tile or image. this.tileIntraBlockCopy = Av1DefaultDistributions.IntraBlockCopy.CreateCopy(); + this.displacementVector = new(); this.switchableRestoration = Av1DefaultDistributions.SwitchableRestoration.CreateCopy(); this.wienerRestoration = Av1DefaultDistributions.WienerRestoration.CreateCopy(); this.sgrProjectionRestoration = Av1DefaultDistributions.SgrProjectionRestoration.CreateCopy(); @@ -229,9 +236,9 @@ internal ref struct Av1SymbolDecoder this.chromaFromLumaSign = Av1DefaultDistributions.ChromaFromLumaSign.CreateCopy(); this.chromaFromLumaAlpha = Av1Distribution.CreateCopy(Av1DefaultDistributions.ChromaFromLumaAlpha); this.intraExtendedTransform = Av1Distribution.CreateCopy(Av1DefaultDistributions.IntraExtendedTransform); + this.interExtendedTransform = Av1Distribution.CreateCopy(Av1DefaultDistributions.InterExtendedTransform); this.configuration = configuration; this.reader = new Av1SymbolReader(tileData, updateCdf); - this.baseQIndex = qIndex; this.endOfBlockFlag = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetEndOfBlockFlag(qIndex)); this.coefficientsBase = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetCoefficientsBase(qIndex)); this.baseEndOfBlock = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetBaseEndOfBlock(qIndex)); @@ -448,6 +455,14 @@ internal ref struct Av1SymbolDecoder return r.ReadSymbol(this.tileIntraBlockCopy) > 0; } + /// + /// Reads an integer intra-block-copy displacement vector relative to a spatial reference. + /// + /// The spatially derived reference vector. + /// The decoded displacement vector in one-eighth-sample units. + public Av1MotionVector ReadDisplacementVector(Av1MotionVector reference) + => this.displacementVector.Read(ref this.reader, reference); + /// /// Reads a complete block partition type from the selected partition context. /// @@ -665,43 +680,54 @@ internal ref struct Av1SymbolDecoder } /// - /// Reads an intra transform type from the transform set permitted for the block. + /// Reads a transform type from the transform set permitted for the block. /// /// The coded transform size. /// Indicates whether the frame restricts transform choices. + /// Indicates whether the block uses inter prediction. /// Indicates whether filter-intra prediction selected the intra direction. - /// The active base quantizer index. + /// Indicates whether the active segment uses lossless transforms. /// The filter-intra mode when enabled. /// The ordinary intra prediction mode. /// The decoded transform type, or DCT-DCT when no transform type is signaled. public Av1TransformType ReadTransformType( Av1TransformSize transformSize, bool useReducedTransformSet, + bool isInter, bool useFilterIntra, - int baseQIndex, + bool isLossless, Av1FilterIntraMode filterIntraMode, Av1PredictionMode intraDirection) { Av1TransformType transformType = Av1TransformType.DctDct; - // A zero base quantizer selects DCT-DCT and carries no transform-type symbol in this intra path. - if (baseQIndex == 0) + // A lossless segment selects DCT-DCT and carries no transform-type symbol. + if (isLossless) { return transformType; } - // Still-image decoding reaches this path only for intra blocks, so the intra transform set is authoritative. - Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType(transformSize, useReducedTransformSet); - if (transformSetType > Av1TransformSetType.DctOnly && baseQIndex > 0) + Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType(transformSize, isInter, useReducedTransformSet); + if (transformSetType > Av1TransformSetType.DctOnly) { - int extendedSet = Av1SymbolContextHelper.GetExtendedTransformSet(transformSetType); + int extendedSet = Av1SymbolContextHelper.GetExtendedTransformSet(transformSetType, isInter); Av1TransformSize squareTransformSize = transformSize.GetSquareSize(); - Av1PredictionMode intraMode = useFilterIntra - ? filterIntraMode.ToIntraDirection() - : intraDirection; ref Av1SymbolReader r = ref this.reader; - int symbol = r.ReadSymbol(this.intraExtendedTransform[extendedSet][(int)squareTransformSize][(int)intraMode]); - transformType = Av1SymbolContextHelper.ExtendedTransformInverse[(int)transformSetType][symbol]; + int symbol; + if (isInter) + { + symbol = r.ReadSymbol(this.interExtendedTransform[extendedSet][(int)squareTransformSize]); + } + else + { + Av1PredictionMode intraMode = useFilterIntra + ? filterIntraMode.ToIntraDirection() + : intraDirection; + + symbol = r.ReadSymbol(this.intraExtendedTransform[extendedSet][(int)squareTransformSize][(int)intraMode]); + } + + transformType = Av1SymbolContextHelper.GetExtendedTransformType(transformSetType, symbol); } return transformType; @@ -769,6 +795,7 @@ internal ref struct Av1SymbolDecoder /// The signaled transform size. /// Indicates whether the active segment is lossless. /// Indicates whether the frame restricts transform choices. + /// The luma transform type shared by inter-predicted chroma. /// The transform descriptor updated with the decoded type and coded-block flag. /// The signed distance from the mode block to the right frame edge. /// The signed distance from the mode block to the bottom frame edge. @@ -788,6 +815,7 @@ internal ref struct Av1SymbolDecoder Av1TransformSize transformSize, bool isLossless, bool useReducedTransformSet, + Av1TransformType lumaTransformType, Av1TransformInfo transformInfo, int modeBlocksToRightEdge, int modeBlocksToBottomEdge, @@ -822,13 +850,21 @@ internal ref struct Av1SymbolDecoder transformInfo.Type = this.ReadTransformType( transformSize, useReducedTransformSet, + modeInfo.UseIntraBlockCopy, modeInfo.UseFilterIntra, - this.baseQIndex, + isLossless, modeInfo.FilterIntraMode, modeInfo.YMode); } - transformInfo.Type = ComputeTransformType(planeType, modeInfo, isLossless, transformSize, transformInfo, useReducedTransformSet); + transformInfo.Type = ComputeTransformType( + planeType, + modeInfo, + isLossless, + transformSize, + lumaTransformType, + transformInfo, + useReducedTransformSet); Av1TransformClass transformClass = transformInfo.Type.ToClass(); Av1ScanOrder scanOrder = Av1ScanOrderConstants.GetScanOrder(transformSize, transformInfo.Type); ReadOnlySpan scan = scanOrder.Scan; @@ -1227,10 +1263,18 @@ internal ref struct Av1SymbolDecoder /// The current block prediction modes. /// Indicates whether the active segment is lossless. /// The signaled transform size. + /// The luma transform type shared by inter-predicted chroma. /// The transform descriptor containing the signaled luma type. /// Indicates whether the frame restricts transform choices. /// The transform type valid for the current plane. - private static Av1TransformType ComputeTransformType(Av1PlaneType planeType, Av1BlockModeInfo modeInfo, bool isLossless, Av1TransformSize transformSize, Av1TransformInfo transformInfo, bool useReducedTransformSet) + private static Av1TransformType ComputeTransformType( + Av1PlaneType planeType, + Av1BlockModeInfo modeInfo, + bool isLossless, + Av1TransformSize transformSize, + Av1TransformType lumaTransformType, + Av1TransformInfo transformInfo, + bool useReducedTransformSet) { Av1TransformType transformType = Av1TransformType.DctDct; if (isLossless || transformSize.GetSquareUpSize() > Av1TransformSize.Size32x32) @@ -1243,6 +1287,12 @@ internal ref struct Av1SymbolDecoder { transformType = transformInfo.Type; } + else if (modeInfo.UseIntraBlockCopy) + { + // Intra-block copy follows inter transform rules, so chroma reuses the luma transform type at the + // corresponding luma-grid position rather than deriving a type from the DC chroma mode. + transformType = lumaTransformType; + } else { // Chroma has its own intra mode, so its implicit transform must be derived independently of luma. @@ -1250,7 +1300,11 @@ internal ref struct Av1SymbolDecoder } } - Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType(transformSize, useReducedTransformSet); + Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType( + transformSize, + modeInfo.UseIntraBlockCopy, + useReducedTransformSet); + if (!transformType.IsExtendedSetUsed(transformSetType)) { transformType = Av1TransformType.DctDct; diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs index 574255160..e2b8e14c0 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs @@ -3,6 +3,7 @@ using System.Buffers; using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; @@ -20,6 +21,11 @@ internal class Av1SymbolEncoder : IDisposable /// private readonly Av1Distribution tileIntraBlockCopy; + /// + /// The tile-adaptive integer displacement-vector context. + /// + private readonly Av1DisplacementVectorContext displacementVector = new(); + /// /// The tile-adaptive partition-type distributions. /// @@ -187,6 +193,14 @@ internal class Av1SymbolEncoder : IDisposable w.WriteSymbol(value, this.tileIntraBlockCopy); } + /// + /// Writes an integer intra-block-copy displacement vector relative to a spatial reference. + /// + /// The displacement vector to encode. + /// The spatially derived reference vector. + public void WriteDisplacementVector(Av1MotionVector value, Av1MotionVector reference) + => this.displacementVector.Write(this.writer, value, reference); + /// /// Writes a complete block partition type using the selected partition context. /// @@ -501,7 +515,7 @@ internal class Av1SymbolEncoder : IDisposable Guard.MustBeLessThan((int)squareTransformSize, 4, nameof(squareTransformSize)); ref Av1SymbolWriter w = ref this.writer; w.WriteSymbol( - Av1SymbolContextHelper.ExtendedTransformIndices[(int)transformSetType][(int)transformType], + Av1SymbolContextHelper.GetExtendedTransformIndex(transformSetType, transformType), this.intraExtendedTransform[extendedSet][(int)squareTransformSize][(int)intraDirectionContext]); } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopy.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopy.cs new file mode 100644 index 000000000..d53b8b4b2 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopy.cs @@ -0,0 +1,451 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; + +/// +/// Derives and validates AV1 intra-block-copy displacement vectors. +/// +internal static class Av1IntraBlockCopy +{ + /// + /// The number of surrounding mode-information rows and columns searched for reference vectors. + /// + private const int ReferenceSearchDistance = 3; + + /// + /// The weight separating immediately adjacent candidates from the outer search area. + /// + private const int NearestCandidateWeight = 640; + + /// + /// The number of 64-sample blocks that an intra-block-copy source must precede the active block. + /// + private const int Delay64 = 4; + + /// + /// Finds the spatial reference used to differentially decode an intra-block-copy displacement vector. + /// + /// The current block geometry and decoded neighbors. + /// The active tile boundaries. + /// The superblock width in 4x4 mode-information units. + /// Reusable storage for up to eight unique reference vectors. + /// Reusable storage for the corresponding spatial weights. + /// The nearest nonzero spatial candidate, or the normative tile-relative fallback. + public static Av1MotionVector FindReference( + Av1PartitionInfo partitionInfo, + Av1TileInfo tileInfo, + int superblockModeInfoSize, + Span candidates, + Span weights) + { + Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize; + int width = blockSize.Get4x4WideCount(); + int height = blockSize.Get4x4HighCount(); + int row = partitionInfo.RowIndex; + int column = partitionInfo.ColumnIndex; + int rowAdjustment = height < 2 && (row & 1) != 0 ? 1 : 0; + int columnAdjustment = width < 2 && (column & 1) != 0 ? 1 : 0; + int maximumRowOffset = 0; + int maximumColumnOffset = 0; + + if (partitionInfo.AvailableAbove) + { + maximumRowOffset = height < 2 ? -4 + rowAdjustment : -(ReferenceSearchDistance << 1) + rowAdjustment; + maximumRowOffset = Math.Clamp(maximumRowOffset, tileInfo.ModeInfoRowStart - row, tileInfo.ModeInfoRowEnd - row - 1); + } + + if (partitionInfo.AvailableLeft) + { + maximumColumnOffset = width < 2 ? -4 + columnAdjustment : -(ReferenceSearchDistance << 1) + columnAdjustment; + maximumColumnOffset = Math.Clamp(maximumColumnOffset, tileInfo.ModeInfoColumnStart - column, tileInfo.ModeInfoColumnEnd - column - 1); + } + + int candidateCount = 0; + int processedRows = 0; + int processedColumns = 0; + if (Math.Abs(maximumRowOffset) >= 1) + { + ScanRow(partitionInfo, -1, maximumRowOffset, candidates, weights, ref candidateCount, ref processedRows); + } + + if (Math.Abs(maximumColumnOffset) >= 1) + { + ScanColumn(partitionInfo, -1, maximumColumnOffset, candidates, weights, ref candidateCount, ref processedColumns); + } + + if (HasTopRight(partitionInfo, superblockModeInfoSize)) + { + AddBlock(partitionInfo, -1, width, tileInfo, candidates, weights, ref candidateCount); + } + + int nearestCandidateCount = candidateCount; + for (int index = 0; index < nearestCandidateCount; index++) + { + weights[index] += NearestCandidateWeight; + } + + // The top-left sample begins the outer search region. Sorting the adjacent and outer regions independently + // preserves libaom's nearest/near ordering while still accumulating repeated vectors across both regions. + AddBlock(partitionInfo, -1, -1, tileInfo, candidates, weights, ref candidateCount); + for (int index = 2; index <= ReferenceSearchDistance; index++) + { + int rowOffset = -(index << 1) + 1 + rowAdjustment; + int columnOffset = -(index << 1) + 1 + columnAdjustment; + if (Math.Abs(rowOffset) <= Math.Abs(maximumRowOffset) && Math.Abs(rowOffset) > processedRows) + { + ScanRow(partitionInfo, rowOffset, maximumRowOffset, candidates, weights, ref candidateCount, ref processedRows); + } + + if (Math.Abs(columnOffset) <= Math.Abs(maximumColumnOffset) && Math.Abs(columnOffset) > processedColumns) + { + ScanColumn(partitionInfo, columnOffset, maximumColumnOffset, candidates, weights, ref candidateCount, ref processedColumns); + } + } + + SortByWeight(candidates, weights, 0, nearestCandidateCount); + SortByWeight(candidates, weights, nearestCandidateCount, candidateCount); + + Av1MotionVector reference = candidateCount > 0 ? candidates[0] : default; + if (reference.IsZero && candidateCount > 1) + { + reference = candidates[1]; + } + + if (!reference.IsZero) + { + return reference; + } + + const int modeInfoSampleSize = 1 << Av1Constants.ModeInfoSizeLog2; + const int eighthSampleScale = 8; + int fallbackRow = -modeInfoSampleSize * superblockModeInfoSize * eighthSampleScale; + int fallbackColumn = fallbackRow - (Delay64 * 64 * eighthSampleScale); + + return (row - superblockModeInfoSize) < tileInfo.ModeInfoRowStart + ? new Av1MotionVector(0, fallbackColumn) + : new Av1MotionVector(fallbackRow, 0); + } + + /// + /// Determines whether a decoded displacement vector references an earlier reconstructable block inside the tile. + /// + /// The decoded displacement vector in one-eighth-sample units. + /// The current block geometry. + /// The active tile boundaries. + /// The sequence-level superblock and chroma configuration. + /// when the complete source block is a permitted reference; otherwise, . + public static bool IsValid(Av1MotionVector vector, Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo, ObuSequenceHeader sequenceHeader) + { + const int eighthSampleScale = 8; + const int modeInfoSampleSize = 1 << Av1Constants.ModeInfoSizeLog2; + if ((vector.Row & (eighthSampleScale - 1)) != 0 || (vector.Column & (eighthSampleScale - 1)) != 0 || + vector.Row <= -(1 << 14) || vector.Row >= (1 << 14) || vector.Column <= -(1 << 14) || vector.Column >= (1 << 14)) + { + return false; + } + + int row = partitionInfo.RowIndex; + int column = partitionInfo.ColumnIndex; + int blockWidth = partitionInfo.ModeInfo.BlockSize.GetWidth(); + int blockHeight = partitionInfo.ModeInfo.BlockSize.GetHeight(); + int sourceTop = (row * modeInfoSampleSize * eighthSampleScale) + vector.Row; + int sourceLeft = (column * modeInfoSampleSize * eighthSampleScale) + vector.Column; + int sourceBottom = (((row * modeInfoSampleSize) + blockHeight) * eighthSampleScale) + vector.Row; + int sourceRight = (((column * modeInfoSampleSize) + blockWidth) * eighthSampleScale) + vector.Column; + int tileTop = tileInfo.ModeInfoRowStart * modeInfoSampleSize * eighthSampleScale; + int tileLeft = tileInfo.ModeInfoColumnStart * modeInfoSampleSize * eighthSampleScale; + int tileBottom = tileInfo.ModeInfoRowEnd * modeInfoSampleSize * eighthSampleScale; + int tileRight = tileInfo.ModeInfoColumnEnd * modeInfoSampleSize * eighthSampleScale; + if (sourceTop < tileTop || sourceLeft < tileLeft || sourceBottom > tileBottom || sourceRight > tileRight) + { + return false; + } + + ObuColorConfig colorConfig = sequenceHeader.ColorConfig; + if (partitionInfo.IsChroma && colorConfig.PlaneCount > 1) + { + // A sub-8x8 luma block can map to a chroma block whose rounded origin lies one additional luma unit + // inside the tile. These checks prevent that chroma reference from crossing the tile boundary. + if (blockWidth < 8 && colorConfig.SubSamplingX && sourceLeft < tileLeft + (modeInfoSampleSize * eighthSampleScale)) + { + return false; + } + + if (blockHeight < 8 && colorConfig.SubSamplingY && sourceTop < tileTop + (modeInfoSampleSize * eighthSampleScale)) + { + return false; + } + } + + int superblockModeInfoSize = sequenceHeader.SuperblockModeInfoSize; + int superblockSize = superblockModeInfoSize * modeInfoSampleSize; + int superblockModeInfoSizeLog2 = sequenceHeader.SuperblockSizeLog2 - Av1Constants.ModeInfoSizeLog2; + int activeSuperblockRow = row >> superblockModeInfoSizeLog2; + int active64Column = (column * modeInfoSampleSize) >> 6; + int sourceSuperblockRow = ((sourceBottom >> 3) - 1) / superblockSize; + int source64Column = ((sourceRight >> 3) - 1) >> 6; + int tile64ColumnCount = ((tileInfo.ModeInfoColumnEnd - tileInfo.ModeInfoColumnStart - 1) >> 4) + 1; + int active64 = (activeSuperblockRow * tile64ColumnCount) + active64Column; + int source64 = (sourceSuperblockRow * tile64ColumnCount) + source64Column; + if (source64 >= active64 - Delay64) + { + return false; + } + + // The wavefront boundary reserves four completed 64-sample columns and advances farther right for every + // completed source row. A 128x128 superblock adds one column to account for its two 64-sample halves. + int gradient = 1 + Delay64 + (superblockSize > 64 ? 1 : 0); + int wavefrontOffset = gradient * (activeSuperblockRow - sourceSuperblockRow); + return sourceSuperblockRow <= activeSuperblockRow && source64Column < active64Column - Delay64 + wavefrontOffset; + } + + /// + /// Scans a mode-information row using AV1's block-size-dependent steps and weights. + /// + private static void ScanRow( + Av1PartitionInfo partitionInfo, + int rowOffset, + int maximumRowOffset, + Span candidates, + Span weights, + ref int candidateCount, + ref int processedRows) + { + int width = partitionInfo.ModeInfo.BlockSize.Get4x4WideCount(); + int end = Math.Min(partitionInfo.GetMaxBlockWide(partitionInfo.ModeInfo.BlockSize, false), 16); + int columnOffset = 0; + if (Math.Abs(rowOffset) > 1) + { + columnOffset = 1; + if ((partitionInfo.ColumnIndex & 1) != 0 && width < 2) + { + columnOffset--; + } + } + + bool useFourUnitStep = width >= 4; + for (int index = 0; index < end;) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt( + new Point(partitionInfo.ColumnIndex + columnOffset + index, partitionInfo.RowIndex + rowOffset)); + + int candidateWidth = candidate.BlockSize.Get4x4WideCount(); + int length = Math.Min(width, candidateWidth); + if (useFourUnitStep) + { + length = Math.Max(4, length); + } + else if (Math.Abs(rowOffset) > 1) + { + length = Math.Max(2, length); + } + + int weight = 2; + if (width >= 2 && width <= candidateWidth) + { + int increment = Math.Min(-maximumRowOffset + rowOffset + 1, candidate.BlockSize.Get4x4HighCount()); + weight = Math.Max(weight, increment); + processedRows = increment - rowOffset - 1; + } + + AddCandidate(candidate, length * weight, candidates, weights, ref candidateCount); + index += length; + } + } + + /// + /// Scans a mode-information column using AV1's block-size-dependent steps and weights. + /// + private static void ScanColumn( + Av1PartitionInfo partitionInfo, + int columnOffset, + int maximumColumnOffset, + Span candidates, + Span weights, + ref int candidateCount, + ref int processedColumns) + { + int height = partitionInfo.ModeInfo.BlockSize.Get4x4HighCount(); + int end = Math.Min(partitionInfo.GetMaxBlockHigh(partitionInfo.ModeInfo.BlockSize, false), 16); + int rowOffset = 0; + if (Math.Abs(columnOffset) > 1) + { + rowOffset = 1; + if ((partitionInfo.RowIndex & 1) != 0 && height < 2) + { + rowOffset--; + } + } + + bool useFourUnitStep = height >= 4; + for (int index = 0; index < end;) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt( + new Point(partitionInfo.ColumnIndex + columnOffset, partitionInfo.RowIndex + rowOffset + index)); + + int candidateHeight = candidate.BlockSize.Get4x4HighCount(); + int length = Math.Min(height, candidateHeight); + if (useFourUnitStep) + { + length = Math.Max(4, length); + } + else if (Math.Abs(columnOffset) > 1) + { + length = Math.Max(2, length); + } + + int weight = 2; + if (height >= 2 && height <= candidateHeight) + { + int increment = Math.Min(-maximumColumnOffset + columnOffset + 1, candidate.BlockSize.Get4x4WideCount()); + weight = Math.Max(weight, increment); + processedColumns = increment - columnOffset - 1; + } + + AddCandidate(candidate, length * weight, candidates, weights, ref candidateCount); + index += length; + } + } + + /// + /// Adds the intra-block-copy vector at one tile-relative search position. + /// + private static void AddBlock( + Av1PartitionInfo partitionInfo, + int rowOffset, + int columnOffset, + Av1TileInfo tileInfo, + Span candidates, + Span weights, + ref int candidateCount) + { + int row = partitionInfo.RowIndex + rowOffset; + int column = partitionInfo.ColumnIndex + columnOffset; + if (row < tileInfo.ModeInfoRowStart || row >= tileInfo.ModeInfoRowEnd || + column < tileInfo.ModeInfoColumnStart || column >= tileInfo.ModeInfoColumnEnd) + { + return; + } + + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column, row)); + AddCandidate(candidate, 4, candidates, weights, ref candidateCount); + } + + /// + /// Accumulates one unique intra-block-copy candidate and its spatial weight. + /// + private static void AddCandidate( + Av1BlockModeInfo candidate, + int weight, + Span candidates, + Span weights, + ref int candidateCount) + { + if (!candidate.UseIntraBlockCopy) + { + return; + } + + Av1MotionVector vector = candidate.DisplacementVector; + int index = 0; + for (; index < candidateCount; index++) + { + if (candidates[index] == vector) + { + weights[index] += weight; + return; + } + } + + if (candidateCount < candidates.Length) + { + candidates[candidateCount] = vector; + weights[candidateCount] = weight; + candidateCount++; + } + } + + /// + /// Sorts one candidate region by descending accumulated weight. + /// + private static void SortByWeight(Span candidates, Span weights, int start, int end) + { + int length = end; + while (length > start) + { + int lastSwap = start; + for (int index = start + 1; index < length; index++) + { + if (weights[index - 1] < weights[index]) + { + Av1MotionVector candidate = candidates[index - 1]; + candidates[index - 1] = candidates[index]; + candidates[index] = candidate; + + int weight = weights[index - 1]; + weights[index - 1] = weights[index]; + weights[index] = weight; + lastSwap = index; + } + } + + length = lastSwap; + } + } + + /// + /// Determines whether the current partition is parsed after the block at its top-right search position. + /// + private static bool HasTopRight(Av1PartitionInfo partitionInfo, int superblockModeInfoSize) + { + int width = partitionInfo.ModeInfo.BlockSize.Get4x4WideCount(); + int height = partitionInfo.ModeInfo.BlockSize.Get4x4HighCount(); + int blockSize = Math.Max(width, height); + if (blockSize > 16) + { + return false; + } + + int row = partitionInfo.RowIndex & (superblockModeInfoSize - 1); + int column = partitionInfo.ColumnIndex & (superblockModeInfoSize - 1); + bool hasTopRight = !((row & blockSize) != 0 && (column & blockSize) != 0); + int traversalSize = blockSize; + while (traversalSize < superblockModeInfoSize) + { + if ((column & traversalSize) == 0) + { + break; + } + + if ((column & (traversalSize << 1)) != 0 && (row & (traversalSize << 1)) != 0) + { + hasTopRight = false; + break; + } + + traversalSize <<= 1; + } + + if (width < height && ((partitionInfo.ColumnIndex + width) & (height - 1)) != 0) + { + hasTopRight = true; + } + + if (width > height && (partitionInfo.RowIndex & (width - 1)) != 0) + { + hasTopRight = false; + } + + // The lower-left square of a vertical-A partition is decoded before its right-hand rectangle. + if (partitionInfo.Type == Av1PartitionType.VerticalA && width == height && (row & traversalSize) != 0) + { + hasTopRight = false; + } + + return hasTopRight; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVector.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVector.cs new file mode 100644 index 000000000..de953309d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVector.cs @@ -0,0 +1,74 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; + +/// +/// Represents an AV1 motion or displacement vector in one-eighth-sample units. +/// +internal readonly struct Av1MotionVector : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The signed vertical displacement in one-eighth-sample units. + /// The signed horizontal displacement in one-eighth-sample units. + public Av1MotionVector(int row, int column) + { + this.Row = row; + this.Column = column; + } + + /// + /// Gets the signed vertical displacement in one-eighth-sample units. + /// + public int Row { get; } + + /// + /// Gets the signed horizontal displacement in one-eighth-sample units. + /// + public int Column { get; } + + /// + /// Gets a value indicating whether both displacement components are zero. + /// + public bool IsZero => this.Row == 0 && this.Column == 0; + + /// + /// Adds a component delta to this vector. + /// + /// The reference vector. + /// The decoded component delta. + /// The component-wise sum. + public static Av1MotionVector operator +(Av1MotionVector value, Av1MotionVector delta) + => new(value.Row + delta.Row, value.Column + delta.Column); + + /// + /// Determines whether two vectors have equal components. + /// + /// The first vector. + /// The second vector. + /// when both components are equal; otherwise, . + public static bool operator ==(Av1MotionVector left, Av1MotionVector right) => left.Equals(right); + + /// + /// Determines whether two vectors have different components. + /// + /// The first vector. + /// The second vector. + /// when either component differs; otherwise, . + public static bool operator !=(Av1MotionVector left, Av1MotionVector right) => !left.Equals(right); + + /// + /// Determines whether this vector has the same components as another vector. + /// + /// The vector to compare. + /// when both components are equal; otherwise, . + public bool Equals(Av1MotionVector other) => this.Row == other.Row && this.Column == other.Column; + + /// + public override bool Equals(object? obj) => obj is Av1MotionVector other && this.Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(this.Row, this.Column); +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.Arithmetic.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.Arithmetic.cs new file mode 100644 index 000000000..222df8a33 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.Arithmetic.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; + +/// +/// Provides the overflow-free rounded-average arithmetic shared by the interpolation operators. +/// +internal static partial class Av1IntraBlockCopyPredictor +{ + /// + /// Computes the AV1 rounded average of two unsigned 8-bit vectors without widening their lanes. + /// + /// The first source vector. + /// The second source vector. + /// The lane-wise rounded averages. + private static Vector128 AverageRounded(Vector128 left, Vector128 right) + => (left | right) - ((left ^ right) >> 1); + + /// + /// Computes the AV1 rounded average of two unsigned 8-bit vectors without widening their lanes. + /// + /// The first source vector. + /// The second source vector. + /// The lane-wise rounded averages. + private static Vector256 AverageRounded(Vector256 left, Vector256 right) + => (left | right) - ((left ^ right) >> 1); + + /// + /// Computes the AV1 rounded average of two unsigned 8-bit vectors without widening their lanes. + /// + /// The first source vector. + /// The second source vector. + /// The lane-wise rounded averages. + private static Vector512 AverageRounded(Vector512 left, Vector512 right) + => (left | right) - ((left ^ right) >> 1); + + /// + /// Computes the AV1 rounded average of two nonnegative high-bit-depth vectors without widening their lanes. + /// + /// The first source vector. + /// The second source vector. + /// The lane-wise rounded averages. + private static Vector128 AverageRounded(Vector128 left, Vector128 right) + { + Vector128 leftUnsigned = left.AsUInt16(); + Vector128 rightUnsigned = right.AsUInt16(); + + // (a | b) - ((a ^ b) >> 1) is ceil((a + b) / 2) without an overflowing lane-wise addition. + return ((leftUnsigned | rightUnsigned) - ((leftUnsigned ^ rightUnsigned) >> 1)).AsInt16(); + } + + /// + /// Computes the AV1 rounded average of two nonnegative high-bit-depth vectors without widening their lanes. + /// + /// The first source vector. + /// The second source vector. + /// The lane-wise rounded averages. + private static Vector256 AverageRounded(Vector256 left, Vector256 right) + { + Vector256 leftUnsigned = left.AsUInt16(); + Vector256 rightUnsigned = right.AsUInt16(); + return ((leftUnsigned | rightUnsigned) - ((leftUnsigned ^ rightUnsigned) >> 1)).AsInt16(); + } + + /// + /// Computes the AV1 rounded average of two nonnegative high-bit-depth vectors without widening their lanes. + /// + /// The first source vector. + /// The second source vector. + /// The lane-wise rounded averages. + private static Vector512 AverageRounded(Vector512 left, Vector512 right) + { + Vector512 leftUnsigned = left.AsUInt16(); + Vector512 rightUnsigned = right.AsUInt16(); + return ((leftUnsigned | rightUnsigned) - ((leftUnsigned ^ rightUnsigned) >> 1)).AsInt16(); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.Operator.cs new file mode 100644 index 000000000..5dddbe2ec --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.Operator.cs @@ -0,0 +1,378 @@ +// 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.Prediction.IntraBlockCopy; + +/// +/// Provides the width-progressive SIMD traversal shared by the intra-block-copy interpolation operators. +/// +internal static partial class Av1IntraBlockCopyPredictor +{ + /// + /// Applies one closed interpolation operator to an 8-bit source block. + /// + /// The source-phase-specific interpolation arithmetic. + private static void Predict( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + where TOperator : struct, IOperator + { + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + + if (Vector128.IsHardwareAccelerated && width is 4 or 8) + { + // AV1 permits 4- and 8-sample transform widths, both smaller than a byte Vector128. The frame allocation's + // 72-sample prediction border makes each full source load readable; exact-width stores avoid touching + // destination padding. + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + Vector128 topLeft = Vector128.LoadUnsafe(ref sourceRow); + Vector128 topRight = TOperator.UsesRight ? Vector128.LoadUnsafe(ref sourceRow, 1) : default; + Vector128 bottomLeft = TOperator.UsesBottom ? Vector128.LoadUnsafe(ref sourceRow, (nuint)sourceStride) : default; + Vector128 bottomRight = TOperator.UsesRight && TOperator.UsesBottom + ? Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + 1)) + : default; + + Vector128 prediction = TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight); + if (width == 8) + { + prediction.GetLower().StoreUnsafe(ref destinationRow); + } + else + { + Unsafe.As(ref destinationRow) = prediction.AsUInt32().GetElement(0); + } + } + + return; + } + + int processedColumns = 0; + + // AV1 transform widths are powers of two. The widest supported tier normally consumes the complete row; the + // cumulative narrower tiers preserve the same contract for future legal widths without over-reading a tail. + if (Vector512.IsHardwareAccelerated) + { + int vectorizedColumns = width - (width % Vector512.Count); + if (vectorizedColumns > 0) + { + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = 0; column < vectorizedColumns; column += Vector512.Count) + { + Vector512 topLeft = Vector512.LoadUnsafe(ref sourceRow, (nuint)column); + Vector512 topRight = TOperator.UsesRight ? Vector512.LoadUnsafe(ref sourceRow, (nuint)(column + 1)) : default; + Vector512 bottomLeft = TOperator.UsesBottom + ? Vector512.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)) + : default; + Vector512 bottomRight = TOperator.UsesRight && TOperator.UsesBottom + ? Vector512.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column + 1)) + : default; + + TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = vectorizedColumns; + } + } + + if (Vector256.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector256.Count); + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector256.Count) + { + Vector256 topLeft = Vector256.LoadUnsafe(ref sourceRow, (nuint)column); + Vector256 topRight = TOperator.UsesRight ? Vector256.LoadUnsafe(ref sourceRow, (nuint)(column + 1)) : default; + Vector256 bottomLeft = TOperator.UsesBottom + ? Vector256.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)) + : default; + Vector256 bottomRight = TOperator.UsesRight && TOperator.UsesBottom + ? Vector256.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column + 1)) + : default; + + TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + + if (Vector128.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector128.Count); + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector128.Count) + { + Vector128 topLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)column); + Vector128 topRight = TOperator.UsesRight ? Vector128.LoadUnsafe(ref sourceRow, (nuint)(column + 1)) : default; + Vector128 bottomLeft = TOperator.UsesBottom + ? Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)) + : default; + Vector128 bottomRight = TOperator.UsesRight && TOperator.UsesBottom + ? Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column + 1)) + : default; + + TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + + // FeatureTestRunner can disable every intrinsic tier. Keeping the scalar continuation in the same traversal + // proves the fallback without changing source addressing or the normative rounding performed by the operator. + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < width; column++) + { + byte topLeft = Unsafe.Add(ref sourceRow, column); + byte topRight = TOperator.UsesRight ? Unsafe.Add(ref sourceRow, column + 1) : default; + byte bottomLeft = TOperator.UsesBottom ? Unsafe.Add(ref sourceRow, sourceStride + column) : default; + byte bottomRight = TOperator.UsesRight && TOperator.UsesBottom + ? Unsafe.Add(ref sourceRow, sourceStride + column + 1) + : default; + + Unsafe.Add(ref destinationRow, column) = TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight); + } + } + } + + /// + /// Applies one closed interpolation operator to a high-bit-depth source block. + /// + /// The source-phase-specific interpolation arithmetic. + private static void Predict( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + where TOperator : struct, IOperator + { + ref short sourceBase = ref MemoryMarshal.GetReference(source); + ref short destinationBase = ref MemoryMarshal.GetReference(destination); + + if (Vector128.IsHardwareAccelerated && width == 4) + { + // Four high-bit-depth samples occupy the lower half of a Vector128. The frame allocation's prediction + // border makes the full source load readable; storing only the lower four lanes avoids relying on writable + // samples beyond the transform boundary. + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + Vector128 topLeft = Vector128.LoadUnsafe(ref sourceRow); + Vector128 topRight = TOperator.UsesRight ? Vector128.LoadUnsafe(ref sourceRow, 1) : default; + Vector128 bottomLeft = TOperator.UsesBottom ? Vector128.LoadUnsafe(ref sourceRow, (nuint)sourceStride) : default; + Vector128 bottomRight = TOperator.UsesRight && TOperator.UsesBottom + ? Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + 1)) + : default; + + TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).GetLower().StoreUnsafe(ref destinationRow); + } + + return; + } + + int processedColumns = 0; + + // High-bit-depth lanes hold half as many samples, but retain the same descending-width traversal and one scalar + // continuation as the byte path. + if (Vector512.IsHardwareAccelerated) + { + int vectorizedColumns = width - (width % Vector512.Count); + if (vectorizedColumns > 0) + { + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = 0; column < vectorizedColumns; column += Vector512.Count) + { + Vector512 topLeft = Vector512.LoadUnsafe(ref sourceRow, (nuint)column); + Vector512 topRight = TOperator.UsesRight ? Vector512.LoadUnsafe(ref sourceRow, (nuint)(column + 1)) : default; + Vector512 bottomLeft = TOperator.UsesBottom + ? Vector512.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)) + : default; + Vector512 bottomRight = TOperator.UsesRight && TOperator.UsesBottom + ? Vector512.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column + 1)) + : default; + + TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = vectorizedColumns; + } + } + + if (Vector256.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector256.Count); + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector256.Count) + { + Vector256 topLeft = Vector256.LoadUnsafe(ref sourceRow, (nuint)column); + Vector256 topRight = TOperator.UsesRight ? Vector256.LoadUnsafe(ref sourceRow, (nuint)(column + 1)) : default; + Vector256 bottomLeft = TOperator.UsesBottom + ? Vector256.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)) + : default; + Vector256 bottomRight = TOperator.UsesRight && TOperator.UsesBottom + ? Vector256.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column + 1)) + : default; + + TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + + if (Vector128.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector128.Count); + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector128.Count) + { + Vector128 topLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)column); + Vector128 topRight = TOperator.UsesRight ? Vector128.LoadUnsafe(ref sourceRow, (nuint)(column + 1)) : default; + Vector128 bottomLeft = TOperator.UsesBottom + ? Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)) + : default; + Vector128 bottomRight = TOperator.UsesRight && TOperator.UsesBottom + ? Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column + 1)) + : default; + + TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < width; column++) + { + short topLeft = Unsafe.Add(ref sourceRow, column); + short topRight = TOperator.UsesRight ? Unsafe.Add(ref sourceRow, column + 1) : default; + short bottomLeft = TOperator.UsesBottom ? Unsafe.Add(ref sourceRow, sourceStride + column) : default; + short bottomRight = TOperator.UsesRight && TOperator.UsesBottom + ? Unsafe.Add(ref sourceRow, sourceStride + column + 1) + : default; + + Unsafe.Add(ref destinationRow, column) = TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight); + } + } + } + + /// + /// Applies one closed interpolation operator to an 8-bit source block without explicit hardware intrinsics. + /// + /// The source-phase-specific interpolation arithmetic. + private static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + where TOperator : struct, IOperator + { + for (int row = 0; row < height; row++) + { + int sourceRow = row * sourceStride; + int destinationRow = row * destinationStride; + + for (int column = 0; column < width; column++) + { + byte topLeft = source[sourceRow + column]; + byte topRight = TOperator.UsesRight ? source[sourceRow + column + 1] : default; + byte bottomLeft = TOperator.UsesBottom ? source[sourceRow + sourceStride + column] : default; + byte bottomRight = TOperator.UsesRight && TOperator.UsesBottom ? source[sourceRow + sourceStride + column + 1] : default; + destination[destinationRow + column] = TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight); + } + } + } + + /// + /// Applies one closed interpolation operator to a high-bit-depth source block without explicit hardware intrinsics. + /// + /// The source-phase-specific interpolation arithmetic. + private static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + where TOperator : struct, IOperator + { + for (int row = 0; row < height; row++) + { + int sourceRow = row * sourceStride; + int destinationRow = row * destinationStride; + + for (int column = 0; column < width; column++) + { + short topLeft = source[sourceRow + column]; + short topRight = TOperator.UsesRight ? source[sourceRow + column + 1] : default; + short bottomLeft = TOperator.UsesBottom ? source[sourceRow + sourceStride + column] : default; + short bottomRight = TOperator.UsesRight && TOperator.UsesBottom ? source[sourceRow + sourceStride + column + 1] : default; + destination[destinationRow + column] = TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.OperatorContract.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.OperatorContract.cs new file mode 100644 index 000000000..48aea9f3b --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.OperatorContract.cs @@ -0,0 +1,136 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; + +/// +/// Defines the scalar and SIMD contract for closed intra-block-copy filter operators. +/// +internal static partial class Av1IntraBlockCopyPredictor +{ + /// + /// Defines lane-wise arithmetic for one intra-block-copy filter phase. + /// + /// + /// Every SIMD lane corresponds to one output column. The generic traversal supplies the integer source sample and + /// its right, lower, and lower-right neighbors; closed operator types allow the JIT to remove unused source loads. + /// + private interface IOperator + { + /// + /// Gets a value indicating whether the operator consumes the source sample to the right. + /// + public static abstract bool UsesRight { get; } + + /// + /// Gets a value indicating whether the operator consumes the source sample on the following row. + /// + public static abstract bool UsesBottom { get; } + + /// + /// Filters one 8-bit sample. + /// + /// The integer-position source sample. + /// The source sample one column to the right. + /// The source sample one row below. + /// The source sample one row below and one column to the right. + /// The filtered 8-bit sample. + public static abstract byte Filter(byte topLeft, byte topRight, byte bottomLeft, byte bottomRight); + + /// + /// Filters sixteen 8-bit samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one column to the right. + /// The source samples one row below. + /// The source samples one row below and one column to the right. + /// The filtered 8-bit samples. + public static abstract Vector128 Filter( + Vector128 topLeft, + Vector128 topRight, + Vector128 bottomLeft, + Vector128 bottomRight); + + /// + /// Filters thirty-two 8-bit samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one column to the right. + /// The source samples one row below. + /// The source samples one row below and one column to the right. + /// The filtered 8-bit samples. + public static abstract Vector256 Filter( + Vector256 topLeft, + Vector256 topRight, + Vector256 bottomLeft, + Vector256 bottomRight); + + /// + /// Filters sixty-four 8-bit samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one column to the right. + /// The source samples one row below. + /// The source samples one row below and one column to the right. + /// The filtered 8-bit samples. + public static abstract Vector512 Filter( + Vector512 topLeft, + Vector512 topRight, + Vector512 bottomLeft, + Vector512 bottomRight); + + /// + /// Filters one high-bit-depth sample. + /// + /// The integer-position source sample. + /// The source sample one column to the right. + /// The source sample one row below. + /// The source sample one row below and one column to the right. + /// The filtered high-bit-depth sample. + public static abstract short Filter(short topLeft, short topRight, short bottomLeft, short bottomRight); + + /// + /// Filters eight high-bit-depth samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one column to the right. + /// The source samples one row below. + /// The source samples one row below and one column to the right. + /// The filtered high-bit-depth samples. + public static abstract Vector128 Filter( + Vector128 topLeft, + Vector128 topRight, + Vector128 bottomLeft, + Vector128 bottomRight); + + /// + /// Filters sixteen high-bit-depth samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one column to the right. + /// The source samples one row below. + /// The source samples one row below and one column to the right. + /// The filtered high-bit-depth samples. + public static abstract Vector256 Filter( + Vector256 topLeft, + Vector256 topRight, + Vector256 bottomLeft, + Vector256 bottomRight); + + /// + /// Filters thirty-two high-bit-depth samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one column to the right. + /// The source samples one row below. + /// The source samples one row below and one column to the right. + /// The filtered high-bit-depth samples. + public static abstract Vector512 Filter( + Vector512 topLeft, + Vector512 topRight, + Vector512 bottomLeft, + Vector512 bottomRight); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.Operators.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.Operators.cs new file mode 100644 index 000000000..42f2bd993 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.Operators.cs @@ -0,0 +1,279 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; + +/// +/// Defines the closed interpolation operators used by intra-block-copy prediction. +/// +internal static partial class Av1IntraBlockCopyPredictor +{ + /// + /// Averages horizontally adjacent source samples for a half-sample horizontal phase. + /// + private readonly struct HorizontalOperator : IOperator + { + /// + public static bool UsesRight => true; + + /// + public static bool UsesBottom => false; + + /// + public static byte Filter(byte topLeft, byte topRight, byte bottomLeft, byte bottomRight) => (byte)((topLeft + topRight + 1) >> 1); + + /// + public static Vector128 Filter( + Vector128 topLeft, + Vector128 topRight, + Vector128 bottomLeft, + Vector128 bottomRight) + => AverageRounded(topLeft, topRight); + + /// + public static Vector256 Filter( + Vector256 topLeft, + Vector256 topRight, + Vector256 bottomLeft, + Vector256 bottomRight) + => AverageRounded(topLeft, topRight); + + /// + public static Vector512 Filter( + Vector512 topLeft, + Vector512 topRight, + Vector512 bottomLeft, + Vector512 bottomRight) + => AverageRounded(topLeft, topRight); + + /// + public static short Filter(short topLeft, short topRight, short bottomLeft, short bottomRight) => (short)((topLeft + topRight + 1) >> 1); + + /// + public static Vector128 Filter( + Vector128 topLeft, + Vector128 topRight, + Vector128 bottomLeft, + Vector128 bottomRight) + => AverageRounded(topLeft, topRight); + + /// + public static Vector256 Filter( + Vector256 topLeft, + Vector256 topRight, + Vector256 bottomLeft, + Vector256 bottomRight) + => AverageRounded(topLeft, topRight); + + /// + public static Vector512 Filter( + Vector512 topLeft, + Vector512 topRight, + Vector512 bottomLeft, + Vector512 bottomRight) + => AverageRounded(topLeft, topRight); + } + + /// + /// Averages vertically adjacent source samples for a half-sample vertical phase. + /// + private readonly struct VerticalOperator : IOperator + { + /// + public static bool UsesRight => false; + + /// + public static bool UsesBottom => true; + + /// + public static byte Filter(byte topLeft, byte topRight, byte bottomLeft, byte bottomRight) => (byte)((topLeft + bottomLeft + 1) >> 1); + + /// + public static Vector128 Filter( + Vector128 topLeft, + Vector128 topRight, + Vector128 bottomLeft, + Vector128 bottomRight) + => AverageRounded(topLeft, bottomLeft); + + /// + public static Vector256 Filter( + Vector256 topLeft, + Vector256 topRight, + Vector256 bottomLeft, + Vector256 bottomRight) + => AverageRounded(topLeft, bottomLeft); + + /// + public static Vector512 Filter( + Vector512 topLeft, + Vector512 topRight, + Vector512 bottomLeft, + Vector512 bottomRight) + => AverageRounded(topLeft, bottomLeft); + + /// + public static short Filter(short topLeft, short topRight, short bottomLeft, short bottomRight) => (short)((topLeft + bottomLeft + 1) >> 1); + + /// + public static Vector128 Filter( + Vector128 topLeft, + Vector128 topRight, + Vector128 bottomLeft, + Vector128 bottomRight) + => AverageRounded(topLeft, bottomLeft); + + /// + public static Vector256 Filter( + Vector256 topLeft, + Vector256 topRight, + Vector256 bottomLeft, + Vector256 bottomRight) + => AverageRounded(topLeft, bottomLeft); + + /// + public static Vector512 Filter( + Vector512 topLeft, + Vector512 topRight, + Vector512 bottomLeft, + Vector512 bottomRight) + => AverageRounded(topLeft, bottomLeft); + } + + /// + /// Applies the separable two-dimensional interpolation required when both source axes have a half-sample phase. + /// + /// + /// The offsets in libaom's separable two-pass implementation cancel algebraically to + /// (topLeft + topRight + bottomLeft + bottomRight + 2) >> 2, so the closed operator produces the exact + /// result directly without an intermediate image buffer. + /// + /// Byte lanes widen to unsigned 16-bit halves before the four-source sum, while high-bit-depth lanes widen to + /// unsigned 32-bit halves. Narrowing recombines those halves in source-column order after the rounded result has + /// returned to the original sample range. + /// + private readonly struct BilinearOperator : IOperator + { + /// + public static bool UsesRight => true; + + /// + public static bool UsesBottom => true; + + /// + public static byte Filter(byte topLeft, byte topRight, byte bottomLeft, byte bottomRight) + => (byte)((topLeft + topRight + bottomLeft + bottomRight + 2) >> 2); + + /// + public static Vector128 Filter( + Vector128 topLeft, + Vector128 topRight, + Vector128 bottomLeft, + Vector128 bottomRight) + { + (Vector128 topLeftLow, Vector128 topLeftHigh) = Vector128.Widen(topLeft); + (Vector128 topRightLow, Vector128 topRightHigh) = Vector128.Widen(topRight); + (Vector128 bottomLeftLow, Vector128 bottomLeftHigh) = Vector128.Widen(bottomLeft); + (Vector128 bottomRightLow, Vector128 bottomRightHigh) = Vector128.Widen(bottomRight); + + // Four byte samples can sum to 1020, so ushort lanes preserve the complete value before AV1's +2 + // rounding term and divide-by-four shift. Narrowing is exact because the result remains in byte range. + Vector128 low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + Vector128.Create((ushort)2)) >> 2; + Vector128 high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + Vector128.Create((ushort)2)) >> 2; + return Vector128.Narrow(low, high); + } + + /// + public static Vector256 Filter( + Vector256 topLeft, + Vector256 topRight, + Vector256 bottomLeft, + Vector256 bottomRight) + { + (Vector256 topLeftLow, Vector256 topLeftHigh) = Vector256.Widen(topLeft); + (Vector256 topRightLow, Vector256 topRightHigh) = Vector256.Widen(topRight); + (Vector256 bottomLeftLow, Vector256 bottomLeftHigh) = Vector256.Widen(bottomLeft); + (Vector256 bottomRightLow, Vector256 bottomRightHigh) = Vector256.Widen(bottomRight); + Vector256 rounding = Vector256.Create((ushort)2); + Vector256 low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + rounding) >> 2; + Vector256 high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + rounding) >> 2; + return Vector256.Narrow(low, high); + } + + /// + public static Vector512 Filter( + Vector512 topLeft, + Vector512 topRight, + Vector512 bottomLeft, + Vector512 bottomRight) + { + (Vector512 topLeftLow, Vector512 topLeftHigh) = Vector512.Widen(topLeft); + (Vector512 topRightLow, Vector512 topRightHigh) = Vector512.Widen(topRight); + (Vector512 bottomLeftLow, Vector512 bottomLeftHigh) = Vector512.Widen(bottomLeft); + (Vector512 bottomRightLow, Vector512 bottomRightHigh) = Vector512.Widen(bottomRight); + Vector512 rounding = Vector512.Create((ushort)2); + Vector512 low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + rounding) >> 2; + Vector512 high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + rounding) >> 2; + return Vector512.Narrow(low, high); + } + + /// + public static short Filter(short topLeft, short topRight, short bottomLeft, short bottomRight) + => (short)((topLeft + topRight + bottomLeft + bottomRight + 2) >> 2); + + /// + public static Vector128 Filter( + Vector128 topLeft, + Vector128 topRight, + Vector128 bottomLeft, + Vector128 bottomRight) + { + (Vector128 topLeftLow, Vector128 topLeftHigh) = Vector128.Widen(topLeft.AsUInt16()); + (Vector128 topRightLow, Vector128 topRightHigh) = Vector128.Widen(topRight.AsUInt16()); + (Vector128 bottomLeftLow, Vector128 bottomLeftHigh) = Vector128.Widen(bottomLeft.AsUInt16()); + (Vector128 bottomRightLow, Vector128 bottomRightHigh) = Vector128.Widen(bottomRight.AsUInt16()); + + // High-bit-depth storage is signed for integration with transform code, but reconstructed samples are + // nonnegative. Unsigned widening therefore preserves 10- and 12-bit values through the four-input sum. + Vector128 low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + Vector128.Create(2U)) >> 2; + Vector128 high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + Vector128.Create(2U)) >> 2; + return Vector128.Narrow(low, high).AsInt16(); + } + + /// + public static Vector256 Filter( + Vector256 topLeft, + Vector256 topRight, + Vector256 bottomLeft, + Vector256 bottomRight) + { + (Vector256 topLeftLow, Vector256 topLeftHigh) = Vector256.Widen(topLeft.AsUInt16()); + (Vector256 topRightLow, Vector256 topRightHigh) = Vector256.Widen(topRight.AsUInt16()); + (Vector256 bottomLeftLow, Vector256 bottomLeftHigh) = Vector256.Widen(bottomLeft.AsUInt16()); + (Vector256 bottomRightLow, Vector256 bottomRightHigh) = Vector256.Widen(bottomRight.AsUInt16()); + Vector256 rounding = Vector256.Create(2U); + Vector256 low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + rounding) >> 2; + Vector256 high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + rounding) >> 2; + return Vector256.Narrow(low, high).AsInt16(); + } + + /// + public static Vector512 Filter( + Vector512 topLeft, + Vector512 topRight, + Vector512 bottomLeft, + Vector512 bottomRight) + { + (Vector512 topLeftLow, Vector512 topLeftHigh) = Vector512.Widen(topLeft.AsUInt16()); + (Vector512 topRightLow, Vector512 topRightHigh) = Vector512.Widen(topRight.AsUInt16()); + (Vector512 bottomLeftLow, Vector512 bottomLeftHigh) = Vector512.Widen(bottomLeft.AsUInt16()); + (Vector512 bottomRightLow, Vector512 bottomRightHigh) = Vector512.Widen(bottomRight.AsUInt16()); + Vector512 rounding = Vector512.Create(2U); + Vector512 low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + rounding) >> 2; + Vector512 high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + rounding) >> 2; + return Vector512.Narrow(low, high).AsInt16(); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.cs new file mode 100644 index 000000000..7dbdbde4a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.cs @@ -0,0 +1,231 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; + +/// +/// Reconstructs AV1 intra-block-copy predictions from an earlier region of the current frame. +/// +/// +/// Whole-sample luma displacements can map to half-sample chroma positions. The predictor therefore selects direct +/// copy, horizontal two-tap, vertical two-tap, or separable two-dimensional bilinear reconstruction per plane. +/// Filtered paths use the widest preferred SIMD width and retain an explicit scalar fallback for feature-disabled +/// execution. Narrow rows read from the frame buffer's prediction padding but use exact-width stores, so vectorization +/// never depends on writable destination padding. +/// +internal static partial class Av1IntraBlockCopyPredictor +{ + /// + /// Reconstructs an 8-bit intra-block-copy prediction. + /// + /// The source region beginning at the integer sample preceding any half-sample phase. + /// The distance, in samples, between source rows. + /// The destination block origin. + /// The distance, in samples, between destination rows. + /// The prediction width in samples. + /// The prediction height in samples. + /// Indicates whether the horizontal source phase is one half-sample. + /// Indicates whether the vertical source phase is one half-sample. + public static void Predict( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height, + bool halfX, + bool halfY) + { + if (!halfX && !halfY) + { + Copy(source, sourceStride, destination, destinationStride, width, height); + } + else if (halfX && halfY) + { + Predict(source, sourceStride, destination, destinationStride, width, height); + } + else if (halfX) + { + Predict(source, sourceStride, destination, destinationStride, width, height); + } + else + { + Predict(source, sourceStride, destination, destinationStride, width, height); + } + } + + /// + /// Reconstructs a high-bit-depth intra-block-copy prediction. + /// + /// The source region beginning at the integer sample preceding any half-sample phase. + /// The distance, in samples, between source rows. + /// The destination block origin. + /// The distance, in samples, between destination rows. + /// The prediction width in samples. + /// The prediction height in samples. + /// Indicates whether the horizontal source phase is one half-sample. + /// Indicates whether the vertical source phase is one half-sample. + public static void Predict( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height, + bool halfX, + bool halfY) + { + if (!halfX && !halfY) + { + Copy(source, sourceStride, destination, destinationStride, width, height); + } + else if (halfX && halfY) + { + Predict(source, sourceStride, destination, destinationStride, width, height); + } + else if (halfX) + { + Predict(source, sourceStride, destination, destinationStride, width, height); + } + else + { + Predict(source, sourceStride, destination, destinationStride, width, height); + } + } + + /// + /// Reconstructs an 8-bit intra-block-copy prediction without explicit hardware intrinsics. + /// + /// The source region beginning at the integer sample preceding any half-sample phase. + /// The distance, in samples, between source rows. + /// The destination block origin. + /// The distance, in samples, between destination rows. + /// The prediction width in samples. + /// The prediction height in samples. + /// Indicates whether the horizontal source phase is one half-sample. + /// Indicates whether the vertical source phase is one half-sample. + public static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height, + bool halfX, + bool halfY) + { + if (!halfX && !halfY) + { + CopyScalar(source, sourceStride, destination, destinationStride, width, height); + } + else if (halfX && halfY) + { + PredictScalar(source, sourceStride, destination, destinationStride, width, height); + } + else if (halfX) + { + PredictScalar(source, sourceStride, destination, destinationStride, width, height); + } + else + { + PredictScalar(source, sourceStride, destination, destinationStride, width, height); + } + } + + /// + /// Reconstructs a high-bit-depth intra-block-copy prediction without explicit hardware intrinsics. + /// + /// The source region beginning at the integer sample preceding any half-sample phase. + /// The distance, in samples, between source rows. + /// The destination block origin. + /// The distance, in samples, between destination rows. + /// The prediction width in samples. + /// The prediction height in samples. + /// Indicates whether the horizontal source phase is one half-sample. + /// Indicates whether the vertical source phase is one half-sample. + public static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height, + bool halfX, + bool halfY) + { + if (!halfX && !halfY) + { + CopyScalar(source, sourceStride, destination, destinationStride, width, height); + } + else if (halfX && halfY) + { + PredictScalar(source, sourceStride, destination, destinationStride, width, height); + } + else if (halfX) + { + PredictScalar(source, sourceStride, destination, destinationStride, width, height); + } + else + { + PredictScalar(source, sourceStride, destination, destinationStride, width, height); + } + } + + /// + /// Copies an 8-bit whole-sample source block to its destination. + /// + private static void Copy(ReadOnlySpan source, int sourceStride, Span destination, int destinationStride, int width, int height) + { + // Span copying delegates each complete row to the runtime's overlap-safe native-width implementation. The + // displacement validity rules keep source and destination blocks separate, so no intermediate buffer is needed. + for (int row = 0; row < height; row++) + { + source.Slice(row * sourceStride, width).CopyTo(destination.Slice(row * destinationStride, width)); + } + } + + /// + /// Copies a high-bit-depth whole-sample source block to its destination. + /// + private static void Copy(ReadOnlySpan source, int sourceStride, Span destination, int destinationStride, int width, int height) + { + for (int row = 0; row < height; row++) + { + source.Slice(row * sourceStride, width).CopyTo(destination.Slice(row * destinationStride, width)); + } + } + + /// + /// Copies an 8-bit whole-sample source block with scalar sample assignments. + /// + private static void CopyScalar(ReadOnlySpan source, int sourceStride, Span destination, int destinationStride, int width, int height) + { + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + destination[(row * destinationStride) + column] = source[(row * sourceStride) + column]; + } + } + } + + /// + /// Copies a high-bit-depth whole-sample source block with scalar sample assignments. + /// + private static void CopyScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + { + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + destination[(row * destinationStride) + column] = source[(row * sourceStride) + column]; + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs index 0036a83ff..b81ebf44f 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; @@ -124,7 +125,12 @@ internal class Av1BlockModeInfo /// /// Gets or sets a value indicating whether intra block copy is selected. /// - public bool UseUltraBlockCopy { get; set; } + public bool UseIntraBlockCopy { get; set; } + + /// + /// Gets or sets the intra-block-copy displacement vector in one-eighth-sample units. + /// + public Av1MotionVector DisplacementVector { get; set; } /// /// Gets or sets the packed chroma-from-luma alpha magnitude indices. diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionInfo.cs index 53b5da77e..f3707ef99 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionInfo.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionInfo.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; @@ -214,6 +215,34 @@ internal class Av1PartitionInfo } } + /// + /// Gets the luma transform type covering a transform position from a subsampled chroma plane. + /// + /// The transform position in 4x4 units of the target plane. + /// Indicates whether the target plane is horizontally subsampled. + /// Indicates whether the target plane is vertically subsampled. + /// The transform type decoded at the corresponding luma-grid position. + public Av1TransformType GetLumaTransformType(Point planePosition, bool subX, bool subY) + { + int lumaColumn = planePosition.X << (subX ? 1 : 0); + int lumaRow = planePosition.Y << (subY ? 1 : 0); + int first = this.ModeInfo.GetFirstTransformLocation(Av1Plane.Y); + int count = this.ModeInfo.GetTransformUnitCount(Av1Plane.Y); + Span lumaTransforms = this.SuperblockInfo.GetTransformInfoY().Slice(first, count); + foreach (Av1TransformInfo transform in lumaTransforms) + { + int width = transform.Size.Get4x4WideCount(); + int height = transform.Size.Get4x4HighCount(); + if (lumaColumn >= transform.OffsetX && lumaColumn < transform.OffsetX + width && + lumaRow >= transform.OffsetY && lumaRow < transform.OffsetY + height) + { + return transform.Type; + } + } + + throw new InvalidImageContentException("Missing luma transform for inter-predicted chroma."); + } + /// /// Gets the block width clipped to the right frame edge. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs index d2dd295ce..3d43797ee 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; @@ -137,6 +138,16 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable /// private readonly int[] coefficientIndex = []; + /// + /// Reusable storage for the eight spatial displacement-vector candidates permitted by AV1. + /// + private InlineArray8 displacementVectorCandidates; + + /// + /// Reusable storage for the spatial weight associated with each displacement-vector candidate. + /// + private InlineArray8 displacementVectorWeights; + /// /// Provides allocator and decoder configuration to tile entropy decoding. /// @@ -670,7 +681,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable partitionInfo.PopulateModeInfoNeighbors(this.SequenceHeader.ColorConfig); - this.ReadModeInfo(ref reader, partitionInfo); + this.ReadModeInfo(ref reader, partitionInfo, tileInfo); this.ReadPaletteTokens(ref reader, partitionInfo); this.ReadBlockTransformSize(ref reader, modeInfoLocation, partitionInfo, superblockInfo, tileInfo); if (partitionInfo.ModeInfo.Skip) @@ -994,8 +1005,29 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable Av1BlockSize planeBlockSize = partitionInfo.ModeInfo.BlockSize.GetSubsampled(subX, subY); int blocksWide = partitionInfo.GetMaxBlockWide(planeBlockSize, subX); int blocksHigh = partitionInfo.GetMaxBlockHigh(planeBlockSize, subY); - - return reader.ReadCoefficients(partitionInfo.ModeInfo, blockPosition, this.aboveNeighborContext.GetContext(plane), this.leftNeighborContext.GetContext(plane), aboveOffset, leftOffset, plane, blocksWide, blocksHigh, transformBlockContext, transformSize, isLossless, this.FrameHeader.UseReducedTransformSet, transformInfo, partitionInfo.ModeBlockToRightEdge, partitionInfo.ModeBlockToBottomEdge, coefficientBuffer); + Av1TransformType lumaTransformType = partitionInfo.ModeInfo.UseIntraBlockCopy && plane > 0 && !isLossless + ? partitionInfo.GetLumaTransformType(blockPosition, subX, subY) + : Av1TransformType.DctDct; + + return reader.ReadCoefficients( + partitionInfo.ModeInfo, + blockPosition, + this.aboveNeighborContext.GetContext(plane), + this.leftNeighborContext.GetContext(plane), + aboveOffset, + leftOffset, + plane, + blocksWide, + blocksHigh, + transformBlockContext, + transformSize, + isLossless, + this.FrameHeader.UseReducedTransformSet, + lumaTransformType, + transformInfo, + partitionInfo.ModeBlockToRightEdge, + partitionInfo.ModeBlockToBottomEdge, + coefficientBuffer); } /// @@ -1390,11 +1422,12 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable /// /// The tile symbol decoder. /// The current coding block. + /// The active tile boundaries. /// Implements the intra-frame branch of AV1 section 5.11.6. - private void ReadModeInfo(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo) + private void ReadModeInfo(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo) { DebugGuard.IsTrue(this.FrameHeader.FrameType is ObuFrameType.KeyFrame or ObuFrameType.IntraOnlyFrame, "Only INTRA frames supported."); - this.ReadIntraFrameModeInfo(ref reader, partitionInfo); + this.ReadIntraFrameModeInfo(ref reader, partitionInfo, tileInfo); } /// @@ -1402,8 +1435,9 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable /// /// The tile symbol decoder. /// The current coding block and its neighbors. + /// The active tile boundaries. /// Implements AV1 section 5.11.7. - private void ReadIntraFrameModeInfo(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo) + private void ReadIntraFrameModeInfo(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo) { if (this.FrameHeader.SegmentationParameters.SegmentIdPrecedesSkip) { @@ -1436,8 +1470,24 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable if (useIntraBlockCopy) { + partitionInfo.ModeInfo.UseIntraBlockCopy = true; partitionInfo.ModeInfo.YMode = Av1PredictionMode.DC; partitionInfo.ModeInfo.UvMode = Av1PredictionMode.DC; + + Av1MotionVector reference = Av1IntraBlockCopy.FindReference( + partitionInfo, + tileInfo, + this.SequenceHeader.SuperblockModeInfoSize, + this.displacementVectorCandidates, + this.displacementVectorWeights); + + Av1MotionVector displacement = reader.ReadDisplacementVector(reference); + if (!Av1IntraBlockCopy.IsValid(displacement, partitionInfo, tileInfo, this.SequenceHeader)) + { + throw new InvalidImageContentException("Invalid AV1 intra-block-copy displacement vector."); + } + + partitionInfo.ModeInfo.DisplacementVector = displacement; } else { diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs index f5603acbd..25570700b 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; @@ -285,33 +286,103 @@ internal sealed class Av1BlockDecoder : IDisposable } } - // The bounded image-item decoder reconstructs intra-only AV1 still pictures, so every transform unit - // predicts its samples before any coded residual is added. - if (highBitDepth) + // Intra-block copy is signaled on an intra-only frame but follows AV1's inter prediction and transform + // rules. Its validated displacement always references an earlier reconstructed region of this frame. + if (modeInfo.UseIntraBlockCopy) { - this.predictionDecoder.Decode( - partitionInfo, - (Av1Plane)plane, - transformSize, - tileInfo, - highBitDepthTransformBlockReconstructionBuffer, - reconstructionStride, - this.frameBuffer.BitDepth, - transformInfo[0].OffsetX, - transformInfo[0].OffsetY); + // libaom predicts the complete coding block before traversing its residual transforms. The mandatory + // 256-pixel source delay prevents overlap, and the two-tap interpolation is translation-invariant, + // so predicting the matching source rectangle for each transform unit produces the same samples. + Point transformPixelPosition = new( + pixelPosition.X + (transformInfo[0].OffsetX << Av1Constants.ModeInfoSizeLog2), + pixelPosition.Y + (transformInfo[0].OffsetY << Av1Constants.ModeInfoSizeLog2)); + + // Displacement vectors use one-eighth luma-sample units. Converting them to the plane's q4 grid + // leaves luma on an integer sample and can leave subsampled chroma exactly at phase eight. + int sourceColumnQ4 = (transformPixelPosition.X << 4) + + (modeInfo.DisplacementVector.Column << (1 - subX)); + + int sourceRowQ4 = (transformPixelPosition.Y << 4) + + (modeInfo.DisplacementVector.Row << (1 - subY)); + + int sourcePhaseX = sourceColumnQ4 & 15; + int sourcePhaseY = sourceRowQ4 & 15; + DebugGuard.IsTrue(sourcePhaseX is 0 or 8, "Intra-block-copy horizontal phase must be an integer or half sample."); + DebugGuard.IsTrue(sourcePhaseY is 0 or 8, "Intra-block-copy vertical phase must be an integer or half sample."); + + Point sourcePixelPosition = new(sourceColumnQ4 >> 4, sourceRowQ4 >> 4); + int transformWidth = transformSize.GetWidth(); + int transformHeight = transformSize.GetHeight(); + + if (highBitDepth) + { + Span source = this.frameBuffer.DeriveBlockPointer16( + (Av1Plane)plane, + sourcePixelPosition, + subX, + subY, + out int sourceStride); + + Av1IntraBlockCopyPredictor.Predict( + source[sourceStride..], + sourceStride, + highBitDepthTransformBlockReconstructionBuffer[reconstructionStride..], + reconstructionStride, + transformWidth, + transformHeight, + sourcePhaseX != 0, + sourcePhaseY != 0); + } + else + { + Span source = this.frameBuffer.DeriveBlockPointer( + (Av1Plane)plane, + sourcePixelPosition, + subX, + subY, + out int sourceStride); + + Av1IntraBlockCopyPredictor.Predict( + source[sourceStride..], + sourceStride, + transformBlockReconstructionBuffer[reconstructionStride..], + reconstructionStride, + transformWidth, + transformHeight, + sourcePhaseX != 0, + sourcePhaseY != 0); + } } else { - this.predictionDecoder.Decode( - partitionInfo, - (Av1Plane)plane, - transformSize, - tileInfo, - transformBlockReconstructionBuffer, - reconstructionStride, - this.frameBuffer.BitDepth, - transformInfo[0].OffsetX, - transformInfo[0].OffsetY); + // Conventional intra prediction consumes the reference-prefixed destination span before the + // transform residual is reconstructed over its first output row. + if (highBitDepth) + { + this.predictionDecoder.Decode( + partitionInfo, + (Av1Plane)plane, + transformSize, + tileInfo, + highBitDepthTransformBlockReconstructionBuffer, + reconstructionStride, + this.frameBuffer.BitDepth, + transformInfo[0].OffsetX, + transformInfo[0].OffsetY); + } + else + { + this.predictionDecoder.Decode( + partitionInfo, + (Av1Plane)plane, + transformSize, + tileInfo, + transformBlockReconstructionBuffer, + reconstructionStride, + this.frameBuffer.BitDepth, + transformInfo[0].OffsetX, + transformInfo[0].OffsetY); + } } int numberOfCoefficients = 0; diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs index e98cbde83..120035112 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs @@ -44,7 +44,10 @@ public class Av1CoefficientsEntropyTests using IMemoryOwner encoded = encoder.Exit(); Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); - decoder.ReadCoefficients(modeInfo, new Point(0, 0), aboveContexts, leftContexts, 0, 0, 0, 1, 1, transformBlockContext, transformSize, false, true, transformInfo, 0, 0, actuals); + decoder.ReadCoefficients( + modeInfo, new Point(0, 0), aboveContexts, leftContexts, + 0, 0, 0, 1, 1, transformBlockContext, transformSize, + false, true, transformType, transformInfo, 0, 0, actuals); // Assert Assert.Equal(endOfBlock, actuals[0]); @@ -94,7 +97,10 @@ public class Av1CoefficientsEntropyTests Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); int plane = Math.Min((int)componentType, 1); - decoder.ReadCoefficients(modeInfo, new Point(0, 0), aboveContexts, leftContexts, 0, 0, plane, 1, 1, transformBlockContext, transformSize, false, true, transformInfo, 0, 0, actuals); + decoder.ReadCoefficients( + modeInfo, new Point(0, 0), aboveContexts, leftContexts, + 0, 0, plane, 1, 1, transformBlockContext, transformSize, + false, true, transformType, transformInfo, 0, 0, actuals); // Assert Assert.Equal(endOfBlock, actuals[0]); @@ -148,7 +154,10 @@ public class Av1CoefficientsEntropyTests Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); int plane = Math.Min((int)componentType, 1); - decoder.ReadCoefficients(modeInfo, new Point(0, 0), aboveContexts, leftContexts, 0, 0, plane, 1, 1, transformBlockContext, transformSize, false, true, transformInfo, 0, 0, actuals); + decoder.ReadCoefficients( + modeInfo, new Point(0, 0), aboveContexts, leftContexts, + 0, 0, plane, 1, 1, transformBlockContext, transformSize, + false, true, transformType, transformInfo, 0, 0, actuals); // Assert Assert.Equal(endOfBlock, actuals[0]); diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs index f33c5d5e9..fcc05f944 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs @@ -4,6 +4,7 @@ using System.Buffers; using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; @@ -373,7 +374,7 @@ public class Av1EntropyTests Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); for (int i = 0; i < values.Length; i++) { - actuals[i] = decoder.ReadTransformType(transformSizeContext, true, false, BaseQIndex, filterIntraMode, intraDirection); + actuals[i] = decoder.ReadTransformType(transformSizeContext, true, false, false, false, filterIntraMode, intraDirection); } // Assert @@ -557,6 +558,50 @@ public class Av1EntropyTests Assert.Equal(values, actuals); } + /// + /// Verifies integer displacement-vector joints, signs, magnitude classes, and adaptive offset bits. + /// + [Fact] + public void RoundTripIntraBlockCopyDisplacementVectors() + { + Av1MotionVector[] references = + [ + new(0, -4096), + new(-1024, 0), + new(256, -256), + new(0, 0), + new(-2048, 2048), + ]; + + Av1MotionVector[] values = + [ + new(8, -4096), + new(-1040, 24), + new(256, -336), + new(512, 1024), + new(6144, -6144), + ]; + + Configuration configuration = Configuration.Default; + Av1SymbolEncoder encoder = new(configuration, 64, BaseQIndex); + + for (int i = 0; i < values.Length; i++) + { + encoder.WriteDisplacementVector(values[i], references[i]); + } + + using IMemoryOwner encoded = encoder.Exit(); + Av1SymbolDecoder decoder = new(configuration, encoded.GetSpan(), BaseQIndex); + Av1MotionVector[] actual = new Av1MotionVector[values.Length]; + + for (int i = 0; i < actual.Length; i++) + { + actual[i] = decoder.ReadDisplacementVector(references[i]); + } + + Assert.Equal(values, actual); + } + public static TheoryData GetRangeData(int count) { TheoryData result = []; diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyPredictorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyPredictorTests.cs new file mode 100644 index 000000000..55a3dc640 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyPredictorTests.cs @@ -0,0 +1,235 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 intra-block-copy interpolation across the supported hardware-intrinsic configurations. +/// +[Trait("Format", "Heif")] +public class Av1IntraBlockCopyPredictorTests +{ + /// + /// Exercises each SIMD register-width tier and the complete scalar fallback. + /// + private const HwIntrinsics PredictorConfigurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + + /// + /// Verifies all four source phases for 8-bit samples at every AV1 transform size. + /// + [Fact] + public void EightBitPredictionMatchesScalarAcrossIntrinsicWidths() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateEightBitPrediction, PredictorConfigurations); + + /// + /// Verifies all four source phases for high-bit-depth samples at every AV1 transform size. + /// + [Fact] + public void HighBitDepthPredictionMatchesScalarAcrossIntrinsicWidths() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthPrediction, PredictorConfigurations); + + /// + /// Verifies the four normative interpolation equations against independently calculated sample blocks. + /// + [Fact] + public void PredictionMatchesKnownInterpolationValues() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateKnownInterpolationValues, PredictorConfigurations); + + /// + /// Compares the SIMD-first 8-bit implementation with its scalar definition and verifies that row padding is unchanged. + /// + private static void ValidateEightBitPrediction() + { + for (int sizeIndex = 0; sizeIndex < (int)Av1TransformSize.AllSizes; sizeIndex++) + { + Av1TransformSize transformSize = (Av1TransformSize)sizeIndex; + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + int sourceStride = width + 17; + int destinationStride = width + 7; + byte[] source = new byte[sourceStride * (height + 1)]; + + for (int i = 0; i < source.Length; i++) + { + source[i] = (byte)((i * 29) + 17); + } + + for (int phase = 0; phase < 4; phase++) + { + byte[] expected = Enumerable.Repeat((byte)0xA5, destinationStride * height).ToArray(); + byte[] actual = Enumerable.Repeat((byte)0xA5, destinationStride * height).ToArray(); + bool halfX = (phase & 1) != 0; + bool halfY = (phase & 2) != 0; + + Av1IntraBlockCopyPredictor.PredictScalar( + source, + sourceStride, + expected, + destinationStride, + width, + height, + halfX, + halfY); + + Av1IntraBlockCopyPredictor.Predict( + source, + sourceStride, + actual, + destinationStride, + width, + height, + halfX, + halfY); + + Assert.Equal(expected, actual); + } + } + } + + /// + /// Compares the SIMD-first high-bit-depth implementation with its scalar definition and verifies exact-width stores. + /// + private static void ValidateHighBitDepthPrediction() + { + for (int sizeIndex = 0; sizeIndex < (int)Av1TransformSize.AllSizes; sizeIndex++) + { + Av1TransformSize transformSize = (Av1TransformSize)sizeIndex; + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + int sourceStride = width + 9; + int destinationStride = width + 5; + short[] source = new short[sourceStride * (height + 1)]; + + for (int i = 0; i < source.Length; i++) + { + source[i] = (short)(((i * 53) + 31) & 0xFFF); + } + + for (int phase = 0; phase < 4; phase++) + { + short[] expected = Enumerable.Repeat((short)0x5A5A, destinationStride * height).ToArray(); + short[] actual = Enumerable.Repeat((short)0x5A5A, destinationStride * height).ToArray(); + bool halfX = (phase & 1) != 0; + bool halfY = (phase & 2) != 0; + + Av1IntraBlockCopyPredictor.PredictScalar( + source, + sourceStride, + expected, + destinationStride, + width, + height, + halfX, + halfY); + + Av1IntraBlockCopyPredictor.Predict( + source, + sourceStride, + actual, + destinationStride, + width, + height, + halfX, + halfY); + + Assert.Equal(expected, actual); + } + } + } + + /// + /// Applies each source phase to a four-by-four block whose expected results are simple arithmetic progressions. + /// + private static void ValidateKnownInterpolationValues() + { + const int sourceStride = 21; + byte[] source = new byte[sourceStride * 5]; + for (int row = 0; row < 5; row++) + { + for (int column = 0; column < 5; column++) + { + source[(row * sourceStride) + column] = (byte)((row * 20) + (column * 4)); + } + } + + ReadOnlySpan copied = + [ + 0, 4, 8, 12, + 20, 24, 28, 32, + 40, 44, 48, 52, + 60, 64, 68, 72, + ]; + + ReadOnlySpan horizontal = + [ + 2, 6, 10, 14, + 22, 26, 30, 34, + 42, 46, 50, 54, + 62, 66, 70, 74, + ]; + + ReadOnlySpan vertical = + [ + 10, 14, 18, 22, + 30, 34, 38, 42, + 50, 54, 58, 62, + 70, 74, 78, 82, + ]; + + ReadOnlySpan bilinear = + [ + 12, 16, 20, 24, + 32, 36, 40, 44, + 52, 56, 60, 64, + 72, 76, 80, 84, + ]; + + ValidateKnownPhase(source, sourceStride, copied, false, false); + ValidateKnownPhase(source, sourceStride, horizontal, true, false); + ValidateKnownPhase(source, sourceStride, vertical, false, true); + ValidateKnownPhase(source, sourceStride, bilinear, true, true); + } + + /// + /// Verifies one four-by-four source phase for both 8-bit and translated high-bit-depth samples. + /// + /// The five-by-five 8-bit source region. + /// The number of source samples per row. + /// The independently calculated four-by-four prediction. + /// Indicates whether the horizontal phase is one half-sample. + /// Indicates whether the vertical phase is one half-sample. + private static void ValidateKnownPhase( + ReadOnlySpan source, + int sourceStride, + ReadOnlySpan expected, + bool halfX, + bool halfY) + { + byte[] actual = new byte[16]; + Av1IntraBlockCopyPredictor.Predict(source, sourceStride, actual, 4, 4, 4, halfX, halfY); + Assert.Equal(expected, actual); + + const int highBitDepthOffset = 1024; + short[] highBitDepthSource = new short[source.Length]; + short[] highBitDepthExpected = new short[expected.Length]; + short[] highBitDepthActual = new short[16]; + + for (int i = 0; i < source.Length; i++) + { + highBitDepthSource[i] = (short)(source[i] + highBitDepthOffset); + } + + for (int i = 0; i < expected.Length; i++) + { + highBitDepthExpected[i] = (short)(expected[i] + highBitDepthOffset); + } + + Av1IntraBlockCopyPredictor.Predict(highBitDepthSource, sourceStride, highBitDepthActual, 4, 4, 4, halfX, halfY); + Assert.Equal(highBitDepthExpected, highBitDepthActual); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyTests.cs new file mode 100644 index 000000000..5dc2c1943 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyTests.cs @@ -0,0 +1,149 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 intra-block-copy reference derivation and displacement-vector legality rules. +/// +[Trait("Format", "Heif")] +public class Av1IntraBlockCopyTests +{ + /// + /// Verifies the horizontal fallback used in the tile's first superblock row. + /// + [Fact] + public void FindReferenceUsesDelayedHorizontalFallbackInFirstSuperblockRow() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + Av1FrameInfo frameInfo = new(sequenceHeader); + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(5, 0)); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, Point.Empty); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) + { + ColumnIndex = 80, + RowIndex = 0, + }; + + Av1TileInfo tileInfo = CreateTileInfo(); + Av1MotionVector[] candidates = new Av1MotionVector[8]; + int[] weights = new int[8]; + + Av1MotionVector actual = Av1IntraBlockCopy.FindReference( + partitionInfo, + tileInfo, + sequenceHeader.SuperblockModeInfoSize, + candidates, + weights); + + Assert.Equal(new Av1MotionVector(0, -2560), actual); + } + + /// + /// Verifies the vertical fallback used after the tile's first superblock row when spatial candidates are absent. + /// + [Fact] + public void FindReferenceUsesPreviousSuperblockRowFallback() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + Av1FrameInfo frameInfo = new(sequenceHeader); + Av1SuperblockInfo aboveSuperblock = frameInfo.GetSuperblock(new Point(5, 0)); + Av1BlockModeInfo aboveModeInfo = new(Av1BlockSize.Block64x64, Point.Empty); + frameInfo.UpdateModeInfo(aboveModeInfo, aboveSuperblock); + aboveSuperblock.BlockCount++; + + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(5, 1)); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, Point.Empty); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) + { + AvailableAbove = true, + ColumnIndex = 80, + RowIndex = 16, + }; + + Av1TileInfo tileInfo = CreateTileInfo(); + Av1MotionVector[] candidates = new Av1MotionVector[8]; + int[] weights = new int[8]; + + Av1MotionVector actual = Av1IntraBlockCopy.FindReference( + partitionInfo, + tileInfo, + sequenceHeader.SuperblockModeInfoSize, + candidates, + weights); + + Assert.Equal(new Av1MotionVector(-512, 0), actual); + } + + /// + /// Verifies tile bounds, whole-sample precision, the four-block delay, and wavefront ordering. + /// + [Fact] + public void IsValidEnforcesIntraBlockCopySourceRestrictions() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + Av1FrameInfo frameInfo = new(sequenceHeader); + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(8, 2)); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, Point.Empty); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) + { + ColumnIndex = 128, + RowIndex = 32, + }; + + Av1TileInfo tileInfo = CreateTileInfo(); + + // A source five 64-sample columns earlier satisfies both the four-column delay and same-row wavefront limit. + Assert.True(Av1IntraBlockCopy.IsValid(new Av1MotionVector(0, -2560), partitionInfo, tileInfo, sequenceHeader)); + + // Moving the source one 64-sample column to the right reaches the forbidden delay boundary exactly. + Assert.False(Av1IntraBlockCopy.IsValid(new Av1MotionVector(0, -2048), partitionInfo, tileInfo, sequenceHeader)); + Assert.False(Av1IntraBlockCopy.IsValid(new Av1MotionVector(0, -2559), partitionInfo, tileInfo, sequenceHeader)); + Assert.False(Av1IntraBlockCopy.IsValid(new Av1MotionVector(0, -4608), partitionInfo, tileInfo, sequenceHeader)); + Assert.False(Av1IntraBlockCopy.IsValid(new Av1MotionVector(512, -2560), partitionInfo, tileInfo, sequenceHeader)); + } + + /// + /// Creates the 640-by-256, 4:2:0 sequence geometry shared by the displacement tests. + /// + private static ObuSequenceHeader CreateSequenceHeader() + => new() + { + MaxFrameWidth = 640, + MaxFrameHeight = 256, + Use128x128Superblock = false, + ColorConfig = new ObuColorConfig + { + IsMonochrome = false, + SubSamplingX = true, + SubSamplingY = true, + BitDepth = Av1BitDepth.EightBit, + }, + }; + + /// + /// Creates one tile covering the complete test frame. + /// + private static Av1TileInfo CreateTileInfo() + { + ObuFrameHeader frameHeader = new() + { + ModeInfoColumnCount = 160, + ModeInfoRowCount = 64, + TilesInfo = new ObuTileGroupHeader + { + TileColumnCount = 1, + TileRowCount = 1, + TileColumnStartModeInfo = [0, 160], + TileRowStartModeInfo = [0, 64], + }, + }; + + return new Av1TileInfo(0, 0, frameHeader); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SymbolContextTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SymbolContextTests.cs index 2dd7b76d5..cfb812210 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SymbolContextTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SymbolContextTests.cs @@ -38,8 +38,8 @@ public class Av1SymbolContextTests Av1TransformSetType transformSetType = (Av1TransformSetType)setType; // Act - Av1TransformType transformType = Av1SymbolContextHelper.ExtendedTransformInverse[(int)transformSetType][index]; - int actualIndex = Av1SymbolContextHelper.ExtendedTransformIndices[(int)transformSetType][(int)transformType]; + Av1TransformType transformType = Av1SymbolContextHelper.GetExtendedTransformType(transformSetType, index); + int actualIndex = Av1SymbolContextHelper.GetExtendedTransformIndex(transformSetType, transformType); // Assert Assert.Equal(actualIndex, index);