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