Browse Source

Complete AV1 intra-block copy decoding

pull/2633/head
James Jackson-South 7 days ago
parent
commit
cf0c81eb3c
  1. 7
      HEIF_IMPLEMENTATION_PLAN.md
  2. 21
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs
  3. 176
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DisplacementVectorContext.cs
  4. 19
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs
  5. 182
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolContextHelper.cs
  6. 102
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs
  7. 16
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs
  8. 451
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopy.cs
  9. 74
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVector.cs
  10. 80
      src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.Arithmetic.cs
  11. 378
      src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.Operator.cs
  12. 136
      src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.OperatorContract.cs
  13. 279
      src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.Operators.cs
  14. 231
      src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.cs
  15. 8
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs
  16. 29
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionInfo.cs
  17. 62
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs
  18. 117
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs
  19. 15
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs
  20. 47
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs
  21. 235
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyPredictorTests.cs
  22. 149
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyTests.cs
  23. 4
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SymbolContextTests.cs

7
HEIF_IMPLEMENTATION_PLAN.md

@ -29,7 +29,7 @@ Checkboxes may be marked complete only when the implementation and the verificat
## Delivery dashboard ## 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: 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. | | 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. | | 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. | | 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. | | 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. | | 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 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] 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. - [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. - [ ] 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] 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. - [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.

21
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) new(14738, 21678, 25779, 27901, 29024, 30302, 30980, 31843, 32144, 32413, 32520, 32594, 32622, 32656, 32660)
]; ];
/// <summary>
/// Gets the inter transform-type distributions indexed by transform set and square transform-size group.
/// </summary>
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)]
];
/// <summary> /// <summary>
/// Gets the intra transform-type distributions indexed by transform set, transform-size group, and intra prediction mode. /// Gets the intra transform-type distributions indexed by transform set, transform-size group, and intra prediction mode.
/// </summary> /// </summary>

176
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;
/// <summary>
/// Decodes integer intra-block-copy displacement vectors with tile-adaptive AV1 distributions.
/// </summary>
internal sealed class Av1DisplacementVectorContext
{
/// <summary>
/// The number of magnitude classes defined by AV1.
/// </summary>
private const int MagnitudeClassCount = 11;
/// <summary>
/// The number of class-zero integer magnitude bits.
/// </summary>
private const int ClassZeroBitCount = 1;
/// <summary>
/// The tile-adaptive distribution selecting which vector components are nonzero.
/// </summary>
private readonly Av1Distribution joint = new(4096, 11264, 19328);
/// <summary>
/// The tile-adaptive vertical component distributions.
/// </summary>
private readonly Component vertical = new();
/// <summary>
/// The tile-adaptive horizontal component distributions.
/// </summary>
private readonly Component horizontal = new();
/// <summary>
/// Reads an integer displacement vector relative to a spatially derived reference.
/// </summary>
/// <param name="reader">The tile range decoder.</param>
/// <param name="reference">The reference displacement vector.</param>
/// <returns>The decoded displacement vector in one-eighth-sample units.</returns>
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);
}
/// <summary>
/// Writes an integer displacement vector relative to a spatially derived reference.
/// </summary>
/// <param name="writer">The tile range encoder.</param>
/// <param name="value">The displacement vector to encode.</param>
/// <param name="reference">The spatially derived reference vector.</param>
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);
}
}
/// <summary>
/// Stores the adaptive magnitude distributions for one displacement-vector component.
/// </summary>
private sealed class Component
{
/// <summary>
/// The distribution selecting the signed magnitude class.
/// </summary>
private readonly Av1Distribution magnitudeClass = new(28672, 30976, 31858, 32320, 32551, 32656, 32740, 32757, 32762, 32767);
/// <summary>
/// The distribution selecting the sign of a nonzero component.
/// </summary>
private readonly Av1Distribution sign = new(16384);
/// <summary>
/// The distribution selecting either of the two class-zero integer magnitudes.
/// </summary>
private readonly Av1Distribution classZero = new(27648);
/// <summary>
/// The binary distributions that reconstruct larger magnitude offsets from least to most significant bit.
/// </summary>
private readonly Av1Distribution[] offsetBits =
[
new(17408), new(17920), new(18944), new(20480), new(22528),
new(24576), new(28672), new(29952), new(29952), new(30720)
];
/// <summary>
/// Reads one signed integer-precision component.
/// </summary>
/// <param name="reader">The tile range decoder.</param>
/// <returns>The signed component in one-eighth-sample units.</returns>
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;
}
/// <summary>
/// Writes one signed integer-precision component.
/// </summary>
/// <param name="writer">The tile range encoder.</param>
/// <param name="value">The nonzero component in one-eighth-sample units.</param>
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]);
}
}
}
}

19
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs

@ -183,6 +183,25 @@ internal class Av1Distribution
{ {
} }
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a twelve-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
/// <param name="p6">The cumulative threshold following symbol six.</param>
/// <param name="p7">The cumulative threshold following symbol seven.</param>
/// <param name="p8">The cumulative threshold following symbol eight.</param>
/// <param name="p9">The cumulative threshold following symbol nine.</param>
/// <param name="p10">The cumulative threshold following symbol ten.</param>
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)
{
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a thirteen-symbol alphabet. /// Initializes a new instance of the <see cref="Av1Distribution"/> class for a thirteen-symbol alphabet.
/// </summary> /// </summary>

182
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolContextHelper.cs

@ -13,60 +13,103 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
internal static class Av1SymbolContextHelper internal static class Av1SymbolContextHelper
{ {
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
private static ReadOnlySpan<byte> TransformBlockSkipContexts => private const int TransformTypeCount = 16;
/// <summary>
/// The number of AV1 transform sets.
/// </summary>
private const int TransformSetCount = 6;
/// <summary>
/// Gets the mapping from each transform set and transform type to its coded symbol index.
/// </summary>
private static ReadOnlySpan<byte> ExtendedTransformIndices =>
[ [
1, 2, 2, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // DCT only
2, 4, 4, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Inter set 3
2, 4, 4, 4, 5, 1, 3, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Intra set 2
2, 4, 4, 4, 5, 1, 5, 6, 4, 0, 0, 0, 0, 0, 0, 2, 3, 0, 0, 0, 0, // Intra set 1
3, 5, 5, 5, 6 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
]; ];
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public static readonly int[][] ExtendedTransformIndices = [ private static ReadOnlySpan<sbyte> ExtendedTransformSetToIndex =>
[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 0, -1, 2, 1, -1, -1,
[1, 3, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // Intra set 2 0, 3, -1, -1, 2, 1
[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
]; ];
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
private static readonly int[] ExtendedTransformSetToIndex = [0, -1, 2, 1, -1, -1]; private static ReadOnlySpan<Av1TransformType> 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
];
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public static readonly Av1TransformType[][] ExtendedTransformInverse = [ private static ReadOnlySpan<byte> ExtendedTransformTypeCounts =>
[Av1TransformType.DctDct], // DCT only [
[], // Inter set 3 1, 2, 5, 7, 12, 16
[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
]; ];
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public static readonly int[] EndOfBlockOffsetBits = [0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; public static ReadOnlySpan<int> EndOfBlockOffsetBits => [0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
/// <summary> /// <summary>
/// Defines the first coefficient position represented by each end-of-block token. /// Gets the first coefficient position represented by each end-of-block token.
/// </summary> /// </summary>
public static readonly int[] EndOfBlockGroupStart = [0, 1, 2, 3, 5, 9, 17, 33, 65, 129, 257, 513]; public static ReadOnlySpan<int> EndOfBlockGroupStart => [0, 1, 2, 3, 5, 9, 17, 33, 65, 129, 257, 513];
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
private static readonly byte[] EndOfBlockToPositionSmall = [ private static ReadOnlySpan<byte> EndOfBlockToPositionSmall =>
[
0, 1, 2, // 0-2 0, 1, 2, // 0-2
3, 3, // 3-4 3, 3, // 3-4
4, 4, 4, 4, // 5-8 4, 4, 4, 4, // 5-8
@ -75,9 +118,10 @@ internal static class Av1SymbolContextHelper
]; ];
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
private static readonly byte[] EndOfBlockToPositionLarge = [ private static ReadOnlySpan<byte> EndOfBlockToPositionLarge =>
[
6, // place holder 6, // place holder
7, // 33-64 7, // 33-64
8, 8,
@ -97,6 +141,18 @@ internal static class Av1SymbolContextHelper
11 // 513- 11 // 513-
]; ];
/// <summary>
/// Gets the mapping from clipped top and left coefficient-level classes to the transform-block skip context.
/// </summary>
private static ReadOnlySpan<byte> 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
];
/// <summary> /// <summary>
/// Reduces a rectangular transform size to the square context used by transform-size distributions. /// Reduces a rectangular transform size to the square context used by transform-size distributions.
/// </summary> /// </summary>
@ -336,21 +392,43 @@ internal static class Av1SymbolContextHelper
/// <param name="useReducedSet">Indicates whether the frame restricts transform choices.</param> /// <param name="useReducedSet">Indicates whether the frame restricts transform choices.</param>
/// <returns>The permitted transform set.</returns> /// <returns>The permitted transform set.</returns>
internal static Av1TransformSetType GetExtendedTransformSetType(Av1TransformSize transformSize, bool useReducedSet) internal static Av1TransformSetType GetExtendedTransformSetType(Av1TransformSize transformSize, bool useReducedSet)
=> GetExtendedTransformSetType(transformSize, false, useReducedSet);
/// <summary>
/// Selects the transform set permitted for a transform size, prediction class, and reduced-set flag.
/// </summary>
/// <param name="transformSize">The coded transform size.</param>
/// <param name="isInter">Indicates whether the block uses inter prediction.</param>
/// <param name="useReducedSet">Indicates whether the frame restricts transform choices.</param>
/// <returns>The permitted transform set.</returns>
internal static Av1TransformSetType GetExtendedTransformSetType(Av1TransformSize transformSize, bool isInter, bool useReducedSet)
{ {
Av1TransformSize squareUpSize = transformSize.GetSquareUpSize(); Av1TransformSize squareUpSize = transformSize.GetSquareUpSize();
if (squareUpSize >= Av1TransformSize.Size32x32) if (squareUpSize > Av1TransformSize.Size32x32)
{ {
return Av1TransformSetType.DctOnly; return Av1TransformSetType.DctOnly;
} }
if (squareUpSize == Av1TransformSize.Size32x32)
{
return isInter ? Av1TransformSetType.InterSet3 : Av1TransformSetType.DctOnly;
}
if (useReducedSet) if (useReducedSet)
{ {
return Av1TransformSetType.IntraSet2; return isInter ? Av1TransformSetType.InterSet3 : Av1TransformSetType.IntraSet2;
} }
Av1TransformSize squareSize = transformSize.GetSquareSize(); 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;
} }
/// <summary> /// <summary>
@ -418,19 +496,47 @@ internal static class Av1SymbolContextHelper
} }
} }
/// <summary>
/// Gets the coded symbol index for a transform type in a transform set.
/// </summary>
/// <param name="setType">The transform set.</param>
/// <param name="transformType">The transform type.</param>
/// <returns>The coded symbol index.</returns>
public static int GetExtendedTransformIndex(Av1TransformSetType setType, Av1TransformType transformType)
=> ExtendedTransformIndices[((int)setType * TransformTypeCount) + (int)transformType];
/// <summary>
/// Gets the transform type represented by a coded symbol in a transform set.
/// </summary>
/// <param name="setType">The transform set.</param>
/// <param name="symbol">The coded symbol index.</param>
/// <returns>The represented transform type.</returns>
public static Av1TransformType GetExtendedTransformType(Av1TransformSetType setType, int symbol)
=> ExtendedTransformTypes[((int)setType * TransformTypeCount) + symbol];
/// <summary> /// <summary>
/// Gets the number of transform types in a transform set. /// Gets the number of transform types in a transform set.
/// </summary> /// </summary>
/// <param name="setType">The transform set.</param> /// <param name="setType">The transform set.</param>
/// <returns>The number of permitted transform types.</returns> /// <returns>The number of permitted transform types.</returns>
internal static int GetExtendedTransformTypeCount(Av1TransformSetType setType) => ExtendedTransformInverse[(int)setType].Length; internal static int GetExtendedTransformTypeCount(Av1TransformSetType setType) => ExtendedTransformTypeCounts[(int)setType];
/// <summary> /// <summary>
/// Gets the entropy-distribution index for an intra transform set. /// Gets the entropy-distribution index for an intra transform set.
/// </summary> /// </summary>
/// <param name="setType">The transform set.</param> /// <param name="setType">The transform set.</param>
/// <returns>The distribution index, or <c>-1</c> for an inter-only set.</returns> /// <returns>The distribution index, or <c>-1</c> for an inter-only set.</returns>
internal static int GetExtendedTransformSet(Av1TransformSetType setType) => ExtendedTransformSetToIndex[(int)setType]; internal static int GetExtendedTransformSet(Av1TransformSetType setType)
=> GetExtendedTransformSet(setType, false);
/// <summary>
/// Gets the entropy-distribution index for a transform set and prediction class.
/// </summary>
/// <param name="setType">The transform set.</param>
/// <param name="isInter">Indicates whether the block uses inter prediction.</param>
/// <returns>The distribution index, or <c>-1</c> when the set is unavailable for the prediction class.</returns>
internal static int GetExtendedTransformSet(Av1TransformSetType setType, bool isInter)
=> ExtendedTransformSetToIndex[((isInter ? 1 : 0) * TransformSetCount) + (int)setType];
/// <summary> /// <summary>
/// Packs the sign of the DC coefficient into a cumulative-level context value. /// Packs the sign of the DC coefficient into a cumulative-level context value.

102
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // 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;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -23,6 +24,11 @@ internal ref struct Av1SymbolDecoder
/// </summary> /// </summary>
private readonly Av1Distribution tileIntraBlockCopy; private readonly Av1Distribution tileIntraBlockCopy;
/// <summary>
/// The tile-adaptive integer displacement-vector context used by intra-block copy.
/// </summary>
private readonly Av1DisplacementVectorContext displacementVector;
/// <summary> /// <summary>
/// The tile-adaptive switchable loop-restoration distribution. /// The tile-adaptive switchable loop-restoration distribution.
/// </summary> /// </summary>
@ -178,6 +184,11 @@ internal ref struct Av1SymbolDecoder
/// </summary> /// </summary>
private readonly Av1Distribution[][][] intraExtendedTransform; private readonly Av1Distribution[][][] intraExtendedTransform;
/// <summary>
/// The tile-adaptive inter transform-type distributions.
/// </summary>
private readonly Av1Distribution[][] interExtendedTransform;
/// <summary> /// <summary>
/// The configuration providing temporary coefficient-context memory. /// The configuration providing temporary coefficient-context memory.
/// </summary> /// </summary>
@ -188,11 +199,6 @@ internal ref struct Av1SymbolDecoder
/// </summary> /// </summary>
private Av1SymbolReader reader; private Av1SymbolReader reader;
/// <summary>
/// The frame base quantizer used to select coefficient probability models.
/// </summary>
private readonly int baseQIndex;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1SymbolDecoder"/> struct for one AV1 tile. /// Initializes a new instance of the <see cref="Av1SymbolDecoder"/> struct for one AV1 tile.
/// </summary> /// </summary>
@ -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 // 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. // updates change the initial probabilities used to decode the next tile or image.
this.tileIntraBlockCopy = Av1DefaultDistributions.IntraBlockCopy.CreateCopy(); this.tileIntraBlockCopy = Av1DefaultDistributions.IntraBlockCopy.CreateCopy();
this.displacementVector = new();
this.switchableRestoration = Av1DefaultDistributions.SwitchableRestoration.CreateCopy(); this.switchableRestoration = Av1DefaultDistributions.SwitchableRestoration.CreateCopy();
this.wienerRestoration = Av1DefaultDistributions.WienerRestoration.CreateCopy(); this.wienerRestoration = Av1DefaultDistributions.WienerRestoration.CreateCopy();
this.sgrProjectionRestoration = Av1DefaultDistributions.SgrProjectionRestoration.CreateCopy(); this.sgrProjectionRestoration = Av1DefaultDistributions.SgrProjectionRestoration.CreateCopy();
@ -229,9 +236,9 @@ internal ref struct Av1SymbolDecoder
this.chromaFromLumaSign = Av1DefaultDistributions.ChromaFromLumaSign.CreateCopy(); this.chromaFromLumaSign = Av1DefaultDistributions.ChromaFromLumaSign.CreateCopy();
this.chromaFromLumaAlpha = Av1Distribution.CreateCopy(Av1DefaultDistributions.ChromaFromLumaAlpha); this.chromaFromLumaAlpha = Av1Distribution.CreateCopy(Av1DefaultDistributions.ChromaFromLumaAlpha);
this.intraExtendedTransform = Av1Distribution.CreateCopy(Av1DefaultDistributions.IntraExtendedTransform); this.intraExtendedTransform = Av1Distribution.CreateCopy(Av1DefaultDistributions.IntraExtendedTransform);
this.interExtendedTransform = Av1Distribution.CreateCopy(Av1DefaultDistributions.InterExtendedTransform);
this.configuration = configuration; this.configuration = configuration;
this.reader = new Av1SymbolReader(tileData, updateCdf); this.reader = new Av1SymbolReader(tileData, updateCdf);
this.baseQIndex = qIndex;
this.endOfBlockFlag = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetEndOfBlockFlag(qIndex)); this.endOfBlockFlag = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetEndOfBlockFlag(qIndex));
this.coefficientsBase = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetCoefficientsBase(qIndex)); this.coefficientsBase = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetCoefficientsBase(qIndex));
this.baseEndOfBlock = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetBaseEndOfBlock(qIndex)); this.baseEndOfBlock = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetBaseEndOfBlock(qIndex));
@ -448,6 +455,14 @@ internal ref struct Av1SymbolDecoder
return r.ReadSymbol(this.tileIntraBlockCopy) > 0; return r.ReadSymbol(this.tileIntraBlockCopy) > 0;
} }
/// <summary>
/// Reads an integer intra-block-copy displacement vector relative to a spatial reference.
/// </summary>
/// <param name="reference">The spatially derived reference vector.</param>
/// <returns>The decoded displacement vector in one-eighth-sample units.</returns>
public Av1MotionVector ReadDisplacementVector(Av1MotionVector reference)
=> this.displacementVector.Read(ref this.reader, reference);
/// <summary> /// <summary>
/// Reads a complete block partition type from the selected partition context. /// Reads a complete block partition type from the selected partition context.
/// </summary> /// </summary>
@ -665,43 +680,54 @@ internal ref struct Av1SymbolDecoder
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="transformSize">The coded transform size.</param> /// <param name="transformSize">The coded transform size.</param>
/// <param name="useReducedTransformSet">Indicates whether the frame restricts transform choices.</param> /// <param name="useReducedTransformSet">Indicates whether the frame restricts transform choices.</param>
/// <param name="isInter">Indicates whether the block uses inter prediction.</param>
/// <param name="useFilterIntra">Indicates whether filter-intra prediction selected the intra direction.</param> /// <param name="useFilterIntra">Indicates whether filter-intra prediction selected the intra direction.</param>
/// <param name="baseQIndex">The active base quantizer index.</param> /// <param name="isLossless">Indicates whether the active segment uses lossless transforms.</param>
/// <param name="filterIntraMode">The filter-intra mode when enabled.</param> /// <param name="filterIntraMode">The filter-intra mode when enabled.</param>
/// <param name="intraDirection">The ordinary intra prediction mode.</param> /// <param name="intraDirection">The ordinary intra prediction mode.</param>
/// <returns>The decoded transform type, or DCT-DCT when no transform type is signaled.</returns> /// <returns>The decoded transform type, or DCT-DCT when no transform type is signaled.</returns>
public Av1TransformType ReadTransformType( public Av1TransformType ReadTransformType(
Av1TransformSize transformSize, Av1TransformSize transformSize,
bool useReducedTransformSet, bool useReducedTransformSet,
bool isInter,
bool useFilterIntra, bool useFilterIntra,
int baseQIndex, bool isLossless,
Av1FilterIntraMode filterIntraMode, Av1FilterIntraMode filterIntraMode,
Av1PredictionMode intraDirection) Av1PredictionMode intraDirection)
{ {
Av1TransformType transformType = Av1TransformType.DctDct; Av1TransformType transformType = Av1TransformType.DctDct;
// A zero base quantizer selects DCT-DCT and carries no transform-type symbol in this intra path. // A lossless segment selects DCT-DCT and carries no transform-type symbol.
if (baseQIndex == 0) if (isLossless)
{ {
return transformType; return transformType;
} }
// Still-image decoding reaches this path only for intra blocks, so the intra transform set is authoritative. Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType(transformSize, isInter, useReducedTransformSet);
Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType(transformSize, useReducedTransformSet); if (transformSetType > Av1TransformSetType.DctOnly)
if (transformSetType > Av1TransformSetType.DctOnly && baseQIndex > 0)
{ {
int extendedSet = Av1SymbolContextHelper.GetExtendedTransformSet(transformSetType); int extendedSet = Av1SymbolContextHelper.GetExtendedTransformSet(transformSetType, isInter);
Av1TransformSize squareTransformSize = transformSize.GetSquareSize(); Av1TransformSize squareTransformSize = transformSize.GetSquareSize();
Av1PredictionMode intraMode = useFilterIntra
? filterIntraMode.ToIntraDirection()
: intraDirection;
ref Av1SymbolReader r = ref this.reader; ref Av1SymbolReader r = ref this.reader;
int symbol = r.ReadSymbol(this.intraExtendedTransform[extendedSet][(int)squareTransformSize][(int)intraMode]); int symbol;
transformType = Av1SymbolContextHelper.ExtendedTransformInverse[(int)transformSetType][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; return transformType;
@ -769,6 +795,7 @@ internal ref struct Av1SymbolDecoder
/// <param name="transformSize">The signaled transform size.</param> /// <param name="transformSize">The signaled transform size.</param>
/// <param name="isLossless">Indicates whether the active segment is lossless.</param> /// <param name="isLossless">Indicates whether the active segment is lossless.</param>
/// <param name="useReducedTransformSet">Indicates whether the frame restricts transform choices.</param> /// <param name="useReducedTransformSet">Indicates whether the frame restricts transform choices.</param>
/// <param name="lumaTransformType">The luma transform type shared by inter-predicted chroma.</param>
/// <param name="transformInfo">The transform descriptor updated with the decoded type and coded-block flag.</param> /// <param name="transformInfo">The transform descriptor updated with the decoded type and coded-block flag.</param>
/// <param name="modeBlocksToRightEdge">The signed distance from the mode block to the right frame edge.</param> /// <param name="modeBlocksToRightEdge">The signed distance from the mode block to the right frame edge.</param>
/// <param name="modeBlocksToBottomEdge">The signed distance from the mode block to the bottom frame edge.</param> /// <param name="modeBlocksToBottomEdge">The signed distance from the mode block to the bottom frame edge.</param>
@ -788,6 +815,7 @@ internal ref struct Av1SymbolDecoder
Av1TransformSize transformSize, Av1TransformSize transformSize,
bool isLossless, bool isLossless,
bool useReducedTransformSet, bool useReducedTransformSet,
Av1TransformType lumaTransformType,
Av1TransformInfo transformInfo, Av1TransformInfo transformInfo,
int modeBlocksToRightEdge, int modeBlocksToRightEdge,
int modeBlocksToBottomEdge, int modeBlocksToBottomEdge,
@ -822,13 +850,21 @@ internal ref struct Av1SymbolDecoder
transformInfo.Type = this.ReadTransformType( transformInfo.Type = this.ReadTransformType(
transformSize, transformSize,
useReducedTransformSet, useReducedTransformSet,
modeInfo.UseIntraBlockCopy,
modeInfo.UseFilterIntra, modeInfo.UseFilterIntra,
this.baseQIndex, isLossless,
modeInfo.FilterIntraMode, modeInfo.FilterIntraMode,
modeInfo.YMode); 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(); Av1TransformClass transformClass = transformInfo.Type.ToClass();
Av1ScanOrder scanOrder = Av1ScanOrderConstants.GetScanOrder(transformSize, transformInfo.Type); Av1ScanOrder scanOrder = Av1ScanOrderConstants.GetScanOrder(transformSize, transformInfo.Type);
ReadOnlySpan<short> scan = scanOrder.Scan; ReadOnlySpan<short> scan = scanOrder.Scan;
@ -1227,10 +1263,18 @@ internal ref struct Av1SymbolDecoder
/// <param name="modeInfo">The current block prediction modes.</param> /// <param name="modeInfo">The current block prediction modes.</param>
/// <param name="isLossless">Indicates whether the active segment is lossless.</param> /// <param name="isLossless">Indicates whether the active segment is lossless.</param>
/// <param name="transformSize">The signaled transform size.</param> /// <param name="transformSize">The signaled transform size.</param>
/// <param name="lumaTransformType">The luma transform type shared by inter-predicted chroma.</param>
/// <param name="transformInfo">The transform descriptor containing the signaled luma type.</param> /// <param name="transformInfo">The transform descriptor containing the signaled luma type.</param>
/// <param name="useReducedTransformSet">Indicates whether the frame restricts transform choices.</param> /// <param name="useReducedTransformSet">Indicates whether the frame restricts transform choices.</param>
/// <returns>The transform type valid for the current plane.</returns> /// <returns>The transform type valid for the current plane.</returns>
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; Av1TransformType transformType = Av1TransformType.DctDct;
if (isLossless || transformSize.GetSquareUpSize() > Av1TransformSize.Size32x32) if (isLossless || transformSize.GetSquareUpSize() > Av1TransformSize.Size32x32)
@ -1243,6 +1287,12 @@ internal ref struct Av1SymbolDecoder
{ {
transformType = transformInfo.Type; 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 else
{ {
// Chroma has its own intra mode, so its implicit transform must be derived independently of luma. // 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)) if (!transformType.IsExtendedSetUsed(transformSetType))
{ {
transformType = Av1TransformType.DctDct; transformType = Av1TransformType.DctDct;

16
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs

@ -3,6 +3,7 @@
using System.Buffers; using System.Buffers;
using SixLabors.ImageSharp.Formats.Heif.Av1; 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;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -20,6 +21,11 @@ internal class Av1SymbolEncoder : IDisposable
/// </summary> /// </summary>
private readonly Av1Distribution tileIntraBlockCopy; private readonly Av1Distribution tileIntraBlockCopy;
/// <summary>
/// The tile-adaptive integer displacement-vector context.
/// </summary>
private readonly Av1DisplacementVectorContext displacementVector = new();
/// <summary> /// <summary>
/// The tile-adaptive partition-type distributions. /// The tile-adaptive partition-type distributions.
/// </summary> /// </summary>
@ -187,6 +193,14 @@ internal class Av1SymbolEncoder : IDisposable
w.WriteSymbol(value, this.tileIntraBlockCopy); w.WriteSymbol(value, this.tileIntraBlockCopy);
} }
/// <summary>
/// Writes an integer intra-block-copy displacement vector relative to a spatial reference.
/// </summary>
/// <param name="value">The displacement vector to encode.</param>
/// <param name="reference">The spatially derived reference vector.</param>
public void WriteDisplacementVector(Av1MotionVector value, Av1MotionVector reference)
=> this.displacementVector.Write(this.writer, value, reference);
/// <summary> /// <summary>
/// Writes a complete block partition type using the selected partition context. /// Writes a complete block partition type using the selected partition context.
/// </summary> /// </summary>
@ -501,7 +515,7 @@ internal class Av1SymbolEncoder : IDisposable
Guard.MustBeLessThan((int)squareTransformSize, 4, nameof(squareTransformSize)); Guard.MustBeLessThan((int)squareTransformSize, 4, nameof(squareTransformSize));
ref Av1SymbolWriter w = ref this.writer; ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol( w.WriteSymbol(
Av1SymbolContextHelper.ExtendedTransformIndices[(int)transformSetType][(int)transformType], Av1SymbolContextHelper.GetExtendedTransformIndex(transformSetType, transformType),
this.intraExtendedTransform[extendedSet][(int)squareTransformSize][(int)intraDirectionContext]); this.intraExtendedTransform[extendedSet][(int)squareTransformSize][(int)intraDirectionContext]);
} }
} }

451
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;
/// <summary>
/// Derives and validates AV1 intra-block-copy displacement vectors.
/// </summary>
internal static class Av1IntraBlockCopy
{
/// <summary>
/// The number of surrounding mode-information rows and columns searched for reference vectors.
/// </summary>
private const int ReferenceSearchDistance = 3;
/// <summary>
/// The weight separating immediately adjacent candidates from the outer search area.
/// </summary>
private const int NearestCandidateWeight = 640;
/// <summary>
/// The number of 64-sample blocks that an intra-block-copy source must precede the active block.
/// </summary>
private const int Delay64 = 4;
/// <summary>
/// Finds the spatial reference used to differentially decode an intra-block-copy displacement vector.
/// </summary>
/// <param name="partitionInfo">The current block geometry and decoded neighbors.</param>
/// <param name="tileInfo">The active tile boundaries.</param>
/// <param name="superblockModeInfoSize">The superblock width in 4x4 mode-information units.</param>
/// <param name="candidates">Reusable storage for up to eight unique reference vectors.</param>
/// <param name="weights">Reusable storage for the corresponding spatial weights.</param>
/// <returns>The nearest nonzero spatial candidate, or the normative tile-relative fallback.</returns>
public static Av1MotionVector FindReference(
Av1PartitionInfo partitionInfo,
Av1TileInfo tileInfo,
int superblockModeInfoSize,
Span<Av1MotionVector> candidates,
Span<int> 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);
}
/// <summary>
/// Determines whether a decoded displacement vector references an earlier reconstructable block inside the tile.
/// </summary>
/// <param name="vector">The decoded displacement vector in one-eighth-sample units.</param>
/// <param name="partitionInfo">The current block geometry.</param>
/// <param name="tileInfo">The active tile boundaries.</param>
/// <param name="sequenceHeader">The sequence-level superblock and chroma configuration.</param>
/// <returns><see langword="true"/> when the complete source block is a permitted reference; otherwise, <see langword="false"/>.</returns>
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;
}
/// <summary>
/// Scans a mode-information row using AV1's block-size-dependent steps and weights.
/// </summary>
private static void ScanRow(
Av1PartitionInfo partitionInfo,
int rowOffset,
int maximumRowOffset,
Span<Av1MotionVector> candidates,
Span<int> 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;
}
}
/// <summary>
/// Scans a mode-information column using AV1's block-size-dependent steps and weights.
/// </summary>
private static void ScanColumn(
Av1PartitionInfo partitionInfo,
int columnOffset,
int maximumColumnOffset,
Span<Av1MotionVector> candidates,
Span<int> 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;
}
}
/// <summary>
/// Adds the intra-block-copy vector at one tile-relative search position.
/// </summary>
private static void AddBlock(
Av1PartitionInfo partitionInfo,
int rowOffset,
int columnOffset,
Av1TileInfo tileInfo,
Span<Av1MotionVector> candidates,
Span<int> 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);
}
/// <summary>
/// Accumulates one unique intra-block-copy candidate and its spatial weight.
/// </summary>
private static void AddCandidate(
Av1BlockModeInfo candidate,
int weight,
Span<Av1MotionVector> candidates,
Span<int> 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++;
}
}
/// <summary>
/// Sorts one candidate region by descending accumulated weight.
/// </summary>
private static void SortByWeight(Span<Av1MotionVector> candidates, Span<int> 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;
}
}
/// <summary>
/// Determines whether the current partition is parsed after the block at its top-right search position.
/// </summary>
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;
}
}

74
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;
/// <summary>
/// Represents an AV1 motion or displacement vector in one-eighth-sample units.
/// </summary>
internal readonly struct Av1MotionVector : IEquatable<Av1MotionVector>
{
/// <summary>
/// Initializes a new instance of the <see cref="Av1MotionVector"/> struct.
/// </summary>
/// <param name="row">The signed vertical displacement in one-eighth-sample units.</param>
/// <param name="column">The signed horizontal displacement in one-eighth-sample units.</param>
public Av1MotionVector(int row, int column)
{
this.Row = row;
this.Column = column;
}
/// <summary>
/// Gets the signed vertical displacement in one-eighth-sample units.
/// </summary>
public int Row { get; }
/// <summary>
/// Gets the signed horizontal displacement in one-eighth-sample units.
/// </summary>
public int Column { get; }
/// <summary>
/// Gets a value indicating whether both displacement components are zero.
/// </summary>
public bool IsZero => this.Row == 0 && this.Column == 0;
/// <summary>
/// Adds a component delta to this vector.
/// </summary>
/// <param name="value">The reference vector.</param>
/// <param name="delta">The decoded component delta.</param>
/// <returns>The component-wise sum.</returns>
public static Av1MotionVector operator +(Av1MotionVector value, Av1MotionVector delta)
=> new(value.Row + delta.Row, value.Column + delta.Column);
/// <summary>
/// Determines whether two vectors have equal components.
/// </summary>
/// <param name="left">The first vector.</param>
/// <param name="right">The second vector.</param>
/// <returns><see langword="true"/> when both components are equal; otherwise, <see langword="false"/>.</returns>
public static bool operator ==(Av1MotionVector left, Av1MotionVector right) => left.Equals(right);
/// <summary>
/// Determines whether two vectors have different components.
/// </summary>
/// <param name="left">The first vector.</param>
/// <param name="right">The second vector.</param>
/// <returns><see langword="true"/> when either component differs; otherwise, <see langword="false"/>.</returns>
public static bool operator !=(Av1MotionVector left, Av1MotionVector right) => !left.Equals(right);
/// <summary>
/// Determines whether this vector has the same components as another vector.
/// </summary>
/// <param name="other">The vector to compare.</param>
/// <returns><see langword="true"/> when both components are equal; otherwise, <see langword="false"/>.</returns>
public bool Equals(Av1MotionVector other) => this.Row == other.Row && this.Column == other.Column;
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is Av1MotionVector other && this.Equals(other);
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.Row, this.Column);
}

80
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;
/// <content>
/// Provides the overflow-free rounded-average arithmetic shared by the interpolation operators.
/// </content>
internal static partial class Av1IntraBlockCopyPredictor
{
/// <summary>
/// Computes the AV1 rounded average of two unsigned 8-bit vectors without widening their lanes.
/// </summary>
/// <param name="left">The first source vector.</param>
/// <param name="right">The second source vector.</param>
/// <returns>The lane-wise rounded averages.</returns>
private static Vector128<byte> AverageRounded(Vector128<byte> left, Vector128<byte> right)
=> (left | right) - ((left ^ right) >> 1);
/// <summary>
/// Computes the AV1 rounded average of two unsigned 8-bit vectors without widening their lanes.
/// </summary>
/// <param name="left">The first source vector.</param>
/// <param name="right">The second source vector.</param>
/// <returns>The lane-wise rounded averages.</returns>
private static Vector256<byte> AverageRounded(Vector256<byte> left, Vector256<byte> right)
=> (left | right) - ((left ^ right) >> 1);
/// <summary>
/// Computes the AV1 rounded average of two unsigned 8-bit vectors without widening their lanes.
/// </summary>
/// <param name="left">The first source vector.</param>
/// <param name="right">The second source vector.</param>
/// <returns>The lane-wise rounded averages.</returns>
private static Vector512<byte> AverageRounded(Vector512<byte> left, Vector512<byte> right)
=> (left | right) - ((left ^ right) >> 1);
/// <summary>
/// Computes the AV1 rounded average of two nonnegative high-bit-depth vectors without widening their lanes.
/// </summary>
/// <param name="left">The first source vector.</param>
/// <param name="right">The second source vector.</param>
/// <returns>The lane-wise rounded averages.</returns>
private static Vector128<short> AverageRounded(Vector128<short> left, Vector128<short> right)
{
Vector128<ushort> leftUnsigned = left.AsUInt16();
Vector128<ushort> 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();
}
/// <summary>
/// Computes the AV1 rounded average of two nonnegative high-bit-depth vectors without widening their lanes.
/// </summary>
/// <param name="left">The first source vector.</param>
/// <param name="right">The second source vector.</param>
/// <returns>The lane-wise rounded averages.</returns>
private static Vector256<short> AverageRounded(Vector256<short> left, Vector256<short> right)
{
Vector256<ushort> leftUnsigned = left.AsUInt16();
Vector256<ushort> rightUnsigned = right.AsUInt16();
return ((leftUnsigned | rightUnsigned) - ((leftUnsigned ^ rightUnsigned) >> 1)).AsInt16();
}
/// <summary>
/// Computes the AV1 rounded average of two nonnegative high-bit-depth vectors without widening their lanes.
/// </summary>
/// <param name="left">The first source vector.</param>
/// <param name="right">The second source vector.</param>
/// <returns>The lane-wise rounded averages.</returns>
private static Vector512<short> AverageRounded(Vector512<short> left, Vector512<short> right)
{
Vector512<ushort> leftUnsigned = left.AsUInt16();
Vector512<ushort> rightUnsigned = right.AsUInt16();
return ((leftUnsigned | rightUnsigned) - ((leftUnsigned ^ rightUnsigned) >> 1)).AsInt16();
}
}

378
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;
/// <content>
/// Provides the width-progressive SIMD traversal shared by the intra-block-copy interpolation operators.
/// </content>
internal static partial class Av1IntraBlockCopyPredictor
{
/// <summary>
/// Applies one closed interpolation operator to an 8-bit source block.
/// </summary>
/// <typeparam name="TOperator">The source-phase-specific interpolation arithmetic.</typeparam>
private static void Predict<TOperator>(
ReadOnlySpan<byte> source,
int sourceStride,
Span<byte> 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<byte> topLeft = Vector128.LoadUnsafe(ref sourceRow);
Vector128<byte> topRight = TOperator.UsesRight ? Vector128.LoadUnsafe(ref sourceRow, 1) : default;
Vector128<byte> bottomLeft = TOperator.UsesBottom ? Vector128.LoadUnsafe(ref sourceRow, (nuint)sourceStride) : default;
Vector128<byte> bottomRight = TOperator.UsesRight && TOperator.UsesBottom
? Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + 1))
: default;
Vector128<byte> prediction = TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight);
if (width == 8)
{
prediction.GetLower().StoreUnsafe(ref destinationRow);
}
else
{
Unsafe.As<byte, uint>(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<byte>.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<byte>.Count)
{
Vector512<byte> topLeft = Vector512.LoadUnsafe(ref sourceRow, (nuint)column);
Vector512<byte> topRight = TOperator.UsesRight ? Vector512.LoadUnsafe(ref sourceRow, (nuint)(column + 1)) : default;
Vector512<byte> bottomLeft = TOperator.UsesBottom
? Vector512.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column))
: default;
Vector512<byte> 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<byte>.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<byte>.Count)
{
Vector256<byte> topLeft = Vector256.LoadUnsafe(ref sourceRow, (nuint)column);
Vector256<byte> topRight = TOperator.UsesRight ? Vector256.LoadUnsafe(ref sourceRow, (nuint)(column + 1)) : default;
Vector256<byte> bottomLeft = TOperator.UsesBottom
? Vector256.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column))
: default;
Vector256<byte> 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<byte>.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<byte>.Count)
{
Vector128<byte> topLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)column);
Vector128<byte> topRight = TOperator.UsesRight ? Vector128.LoadUnsafe(ref sourceRow, (nuint)(column + 1)) : default;
Vector128<byte> bottomLeft = TOperator.UsesBottom
? Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column))
: default;
Vector128<byte> 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);
}
}
}
/// <summary>
/// Applies one closed interpolation operator to a high-bit-depth source block.
/// </summary>
/// <typeparam name="TOperator">The source-phase-specific interpolation arithmetic.</typeparam>
private static void Predict<TOperator>(
ReadOnlySpan<short> source,
int sourceStride,
Span<short> 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<short> topLeft = Vector128.LoadUnsafe(ref sourceRow);
Vector128<short> topRight = TOperator.UsesRight ? Vector128.LoadUnsafe(ref sourceRow, 1) : default;
Vector128<short> bottomLeft = TOperator.UsesBottom ? Vector128.LoadUnsafe(ref sourceRow, (nuint)sourceStride) : default;
Vector128<short> 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<short>.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<short>.Count)
{
Vector512<short> topLeft = Vector512.LoadUnsafe(ref sourceRow, (nuint)column);
Vector512<short> topRight = TOperator.UsesRight ? Vector512.LoadUnsafe(ref sourceRow, (nuint)(column + 1)) : default;
Vector512<short> bottomLeft = TOperator.UsesBottom
? Vector512.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column))
: default;
Vector512<short> 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<short>.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<short>.Count)
{
Vector256<short> topLeft = Vector256.LoadUnsafe(ref sourceRow, (nuint)column);
Vector256<short> topRight = TOperator.UsesRight ? Vector256.LoadUnsafe(ref sourceRow, (nuint)(column + 1)) : default;
Vector256<short> bottomLeft = TOperator.UsesBottom
? Vector256.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column))
: default;
Vector256<short> 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<short>.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<short>.Count)
{
Vector128<short> topLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)column);
Vector128<short> topRight = TOperator.UsesRight ? Vector128.LoadUnsafe(ref sourceRow, (nuint)(column + 1)) : default;
Vector128<short> bottomLeft = TOperator.UsesBottom
? Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column))
: default;
Vector128<short> 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);
}
}
}
/// <summary>
/// Applies one closed interpolation operator to an 8-bit source block without explicit hardware intrinsics.
/// </summary>
/// <typeparam name="TOperator">The source-phase-specific interpolation arithmetic.</typeparam>
private static void PredictScalar<TOperator>(
ReadOnlySpan<byte> source,
int sourceStride,
Span<byte> 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);
}
}
}
/// <summary>
/// Applies one closed interpolation operator to a high-bit-depth source block without explicit hardware intrinsics.
/// </summary>
/// <typeparam name="TOperator">The source-phase-specific interpolation arithmetic.</typeparam>
private static void PredictScalar<TOperator>(
ReadOnlySpan<short> source,
int sourceStride,
Span<short> 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);
}
}
}
}

136
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;
/// <content>
/// Defines the scalar and SIMD contract for closed intra-block-copy filter operators.
/// </content>
internal static partial class Av1IntraBlockCopyPredictor
{
/// <summary>
/// Defines lane-wise arithmetic for one intra-block-copy filter phase.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
private interface IOperator
{
/// <summary>
/// Gets a value indicating whether the operator consumes the source sample to the right.
/// </summary>
public static abstract bool UsesRight { get; }
/// <summary>
/// Gets a value indicating whether the operator consumes the source sample on the following row.
/// </summary>
public static abstract bool UsesBottom { get; }
/// <summary>
/// Filters one 8-bit sample.
/// </summary>
/// <param name="topLeft">The integer-position source sample.</param>
/// <param name="topRight">The source sample one column to the right.</param>
/// <param name="bottomLeft">The source sample one row below.</param>
/// <param name="bottomRight">The source sample one row below and one column to the right.</param>
/// <returns>The filtered 8-bit sample.</returns>
public static abstract byte Filter(byte topLeft, byte topRight, byte bottomLeft, byte bottomRight);
/// <summary>
/// Filters sixteen 8-bit samples in parallel.
/// </summary>
/// <param name="topLeft">The integer-position source samples.</param>
/// <param name="topRight">The source samples one column to the right.</param>
/// <param name="bottomLeft">The source samples one row below.</param>
/// <param name="bottomRight">The source samples one row below and one column to the right.</param>
/// <returns>The filtered 8-bit samples.</returns>
public static abstract Vector128<byte> Filter(
Vector128<byte> topLeft,
Vector128<byte> topRight,
Vector128<byte> bottomLeft,
Vector128<byte> bottomRight);
/// <summary>
/// Filters thirty-two 8-bit samples in parallel.
/// </summary>
/// <param name="topLeft">The integer-position source samples.</param>
/// <param name="topRight">The source samples one column to the right.</param>
/// <param name="bottomLeft">The source samples one row below.</param>
/// <param name="bottomRight">The source samples one row below and one column to the right.</param>
/// <returns>The filtered 8-bit samples.</returns>
public static abstract Vector256<byte> Filter(
Vector256<byte> topLeft,
Vector256<byte> topRight,
Vector256<byte> bottomLeft,
Vector256<byte> bottomRight);
/// <summary>
/// Filters sixty-four 8-bit samples in parallel.
/// </summary>
/// <param name="topLeft">The integer-position source samples.</param>
/// <param name="topRight">The source samples one column to the right.</param>
/// <param name="bottomLeft">The source samples one row below.</param>
/// <param name="bottomRight">The source samples one row below and one column to the right.</param>
/// <returns>The filtered 8-bit samples.</returns>
public static abstract Vector512<byte> Filter(
Vector512<byte> topLeft,
Vector512<byte> topRight,
Vector512<byte> bottomLeft,
Vector512<byte> bottomRight);
/// <summary>
/// Filters one high-bit-depth sample.
/// </summary>
/// <param name="topLeft">The integer-position source sample.</param>
/// <param name="topRight">The source sample one column to the right.</param>
/// <param name="bottomLeft">The source sample one row below.</param>
/// <param name="bottomRight">The source sample one row below and one column to the right.</param>
/// <returns>The filtered high-bit-depth sample.</returns>
public static abstract short Filter(short topLeft, short topRight, short bottomLeft, short bottomRight);
/// <summary>
/// Filters eight high-bit-depth samples in parallel.
/// </summary>
/// <param name="topLeft">The integer-position source samples.</param>
/// <param name="topRight">The source samples one column to the right.</param>
/// <param name="bottomLeft">The source samples one row below.</param>
/// <param name="bottomRight">The source samples one row below and one column to the right.</param>
/// <returns>The filtered high-bit-depth samples.</returns>
public static abstract Vector128<short> Filter(
Vector128<short> topLeft,
Vector128<short> topRight,
Vector128<short> bottomLeft,
Vector128<short> bottomRight);
/// <summary>
/// Filters sixteen high-bit-depth samples in parallel.
/// </summary>
/// <param name="topLeft">The integer-position source samples.</param>
/// <param name="topRight">The source samples one column to the right.</param>
/// <param name="bottomLeft">The source samples one row below.</param>
/// <param name="bottomRight">The source samples one row below and one column to the right.</param>
/// <returns>The filtered high-bit-depth samples.</returns>
public static abstract Vector256<short> Filter(
Vector256<short> topLeft,
Vector256<short> topRight,
Vector256<short> bottomLeft,
Vector256<short> bottomRight);
/// <summary>
/// Filters thirty-two high-bit-depth samples in parallel.
/// </summary>
/// <param name="topLeft">The integer-position source samples.</param>
/// <param name="topRight">The source samples one column to the right.</param>
/// <param name="bottomLeft">The source samples one row below.</param>
/// <param name="bottomRight">The source samples one row below and one column to the right.</param>
/// <returns>The filtered high-bit-depth samples.</returns>
public static abstract Vector512<short> Filter(
Vector512<short> topLeft,
Vector512<short> topRight,
Vector512<short> bottomLeft,
Vector512<short> bottomRight);
}
}

279
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;
/// <content>
/// Defines the closed interpolation operators used by intra-block-copy prediction.
/// </content>
internal static partial class Av1IntraBlockCopyPredictor
{
/// <summary>
/// Averages horizontally adjacent source samples for a half-sample horizontal phase.
/// </summary>
private readonly struct HorizontalOperator : IOperator
{
/// <inheritdoc/>
public static bool UsesRight => true;
/// <inheritdoc/>
public static bool UsesBottom => false;
/// <inheritdoc/>
public static byte Filter(byte topLeft, byte topRight, byte bottomLeft, byte bottomRight) => (byte)((topLeft + topRight + 1) >> 1);
/// <inheritdoc/>
public static Vector128<byte> Filter(
Vector128<byte> topLeft,
Vector128<byte> topRight,
Vector128<byte> bottomLeft,
Vector128<byte> bottomRight)
=> AverageRounded(topLeft, topRight);
/// <inheritdoc/>
public static Vector256<byte> Filter(
Vector256<byte> topLeft,
Vector256<byte> topRight,
Vector256<byte> bottomLeft,
Vector256<byte> bottomRight)
=> AverageRounded(topLeft, topRight);
/// <inheritdoc/>
public static Vector512<byte> Filter(
Vector512<byte> topLeft,
Vector512<byte> topRight,
Vector512<byte> bottomLeft,
Vector512<byte> bottomRight)
=> AverageRounded(topLeft, topRight);
/// <inheritdoc/>
public static short Filter(short topLeft, short topRight, short bottomLeft, short bottomRight) => (short)((topLeft + topRight + 1) >> 1);
/// <inheritdoc/>
public static Vector128<short> Filter(
Vector128<short> topLeft,
Vector128<short> topRight,
Vector128<short> bottomLeft,
Vector128<short> bottomRight)
=> AverageRounded(topLeft, topRight);
/// <inheritdoc/>
public static Vector256<short> Filter(
Vector256<short> topLeft,
Vector256<short> topRight,
Vector256<short> bottomLeft,
Vector256<short> bottomRight)
=> AverageRounded(topLeft, topRight);
/// <inheritdoc/>
public static Vector512<short> Filter(
Vector512<short> topLeft,
Vector512<short> topRight,
Vector512<short> bottomLeft,
Vector512<short> bottomRight)
=> AverageRounded(topLeft, topRight);
}
/// <summary>
/// Averages vertically adjacent source samples for a half-sample vertical phase.
/// </summary>
private readonly struct VerticalOperator : IOperator
{
/// <inheritdoc/>
public static bool UsesRight => false;
/// <inheritdoc/>
public static bool UsesBottom => true;
/// <inheritdoc/>
public static byte Filter(byte topLeft, byte topRight, byte bottomLeft, byte bottomRight) => (byte)((topLeft + bottomLeft + 1) >> 1);
/// <inheritdoc/>
public static Vector128<byte> Filter(
Vector128<byte> topLeft,
Vector128<byte> topRight,
Vector128<byte> bottomLeft,
Vector128<byte> bottomRight)
=> AverageRounded(topLeft, bottomLeft);
/// <inheritdoc/>
public static Vector256<byte> Filter(
Vector256<byte> topLeft,
Vector256<byte> topRight,
Vector256<byte> bottomLeft,
Vector256<byte> bottomRight)
=> AverageRounded(topLeft, bottomLeft);
/// <inheritdoc/>
public static Vector512<byte> Filter(
Vector512<byte> topLeft,
Vector512<byte> topRight,
Vector512<byte> bottomLeft,
Vector512<byte> bottomRight)
=> AverageRounded(topLeft, bottomLeft);
/// <inheritdoc/>
public static short Filter(short topLeft, short topRight, short bottomLeft, short bottomRight) => (short)((topLeft + bottomLeft + 1) >> 1);
/// <inheritdoc/>
public static Vector128<short> Filter(
Vector128<short> topLeft,
Vector128<short> topRight,
Vector128<short> bottomLeft,
Vector128<short> bottomRight)
=> AverageRounded(topLeft, bottomLeft);
/// <inheritdoc/>
public static Vector256<short> Filter(
Vector256<short> topLeft,
Vector256<short> topRight,
Vector256<short> bottomLeft,
Vector256<short> bottomRight)
=> AverageRounded(topLeft, bottomLeft);
/// <inheritdoc/>
public static Vector512<short> Filter(
Vector512<short> topLeft,
Vector512<short> topRight,
Vector512<short> bottomLeft,
Vector512<short> bottomRight)
=> AverageRounded(topLeft, bottomLeft);
}
/// <summary>
/// Applies the separable two-dimensional interpolation required when both source axes have a half-sample phase.
/// </summary>
/// <remarks>
/// The offsets in libaom's separable two-pass implementation cancel algebraically to
/// <c>(topLeft + topRight + bottomLeft + bottomRight + 2) &gt;&gt; 2</c>, 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.
/// </remarks>
private readonly struct BilinearOperator : IOperator
{
/// <inheritdoc/>
public static bool UsesRight => true;
/// <inheritdoc/>
public static bool UsesBottom => true;
/// <inheritdoc/>
public static byte Filter(byte topLeft, byte topRight, byte bottomLeft, byte bottomRight)
=> (byte)((topLeft + topRight + bottomLeft + bottomRight + 2) >> 2);
/// <inheritdoc/>
public static Vector128<byte> Filter(
Vector128<byte> topLeft,
Vector128<byte> topRight,
Vector128<byte> bottomLeft,
Vector128<byte> bottomRight)
{
(Vector128<ushort> topLeftLow, Vector128<ushort> topLeftHigh) = Vector128.Widen(topLeft);
(Vector128<ushort> topRightLow, Vector128<ushort> topRightHigh) = Vector128.Widen(topRight);
(Vector128<ushort> bottomLeftLow, Vector128<ushort> bottomLeftHigh) = Vector128.Widen(bottomLeft);
(Vector128<ushort> bottomRightLow, Vector128<ushort> 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<ushort> low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + Vector128.Create((ushort)2)) >> 2;
Vector128<ushort> high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + Vector128.Create((ushort)2)) >> 2;
return Vector128.Narrow(low, high);
}
/// <inheritdoc/>
public static Vector256<byte> Filter(
Vector256<byte> topLeft,
Vector256<byte> topRight,
Vector256<byte> bottomLeft,
Vector256<byte> bottomRight)
{
(Vector256<ushort> topLeftLow, Vector256<ushort> topLeftHigh) = Vector256.Widen(topLeft);
(Vector256<ushort> topRightLow, Vector256<ushort> topRightHigh) = Vector256.Widen(topRight);
(Vector256<ushort> bottomLeftLow, Vector256<ushort> bottomLeftHigh) = Vector256.Widen(bottomLeft);
(Vector256<ushort> bottomRightLow, Vector256<ushort> bottomRightHigh) = Vector256.Widen(bottomRight);
Vector256<ushort> rounding = Vector256.Create((ushort)2);
Vector256<ushort> low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + rounding) >> 2;
Vector256<ushort> high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + rounding) >> 2;
return Vector256.Narrow(low, high);
}
/// <inheritdoc/>
public static Vector512<byte> Filter(
Vector512<byte> topLeft,
Vector512<byte> topRight,
Vector512<byte> bottomLeft,
Vector512<byte> bottomRight)
{
(Vector512<ushort> topLeftLow, Vector512<ushort> topLeftHigh) = Vector512.Widen(topLeft);
(Vector512<ushort> topRightLow, Vector512<ushort> topRightHigh) = Vector512.Widen(topRight);
(Vector512<ushort> bottomLeftLow, Vector512<ushort> bottomLeftHigh) = Vector512.Widen(bottomLeft);
(Vector512<ushort> bottomRightLow, Vector512<ushort> bottomRightHigh) = Vector512.Widen(bottomRight);
Vector512<ushort> rounding = Vector512.Create((ushort)2);
Vector512<ushort> low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + rounding) >> 2;
Vector512<ushort> high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + rounding) >> 2;
return Vector512.Narrow(low, high);
}
/// <inheritdoc/>
public static short Filter(short topLeft, short topRight, short bottomLeft, short bottomRight)
=> (short)((topLeft + topRight + bottomLeft + bottomRight + 2) >> 2);
/// <inheritdoc/>
public static Vector128<short> Filter(
Vector128<short> topLeft,
Vector128<short> topRight,
Vector128<short> bottomLeft,
Vector128<short> bottomRight)
{
(Vector128<uint> topLeftLow, Vector128<uint> topLeftHigh) = Vector128.Widen(topLeft.AsUInt16());
(Vector128<uint> topRightLow, Vector128<uint> topRightHigh) = Vector128.Widen(topRight.AsUInt16());
(Vector128<uint> bottomLeftLow, Vector128<uint> bottomLeftHigh) = Vector128.Widen(bottomLeft.AsUInt16());
(Vector128<uint> bottomRightLow, Vector128<uint> 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<uint> low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + Vector128.Create(2U)) >> 2;
Vector128<uint> high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + Vector128.Create(2U)) >> 2;
return Vector128.Narrow(low, high).AsInt16();
}
/// <inheritdoc/>
public static Vector256<short> Filter(
Vector256<short> topLeft,
Vector256<short> topRight,
Vector256<short> bottomLeft,
Vector256<short> bottomRight)
{
(Vector256<uint> topLeftLow, Vector256<uint> topLeftHigh) = Vector256.Widen(topLeft.AsUInt16());
(Vector256<uint> topRightLow, Vector256<uint> topRightHigh) = Vector256.Widen(topRight.AsUInt16());
(Vector256<uint> bottomLeftLow, Vector256<uint> bottomLeftHigh) = Vector256.Widen(bottomLeft.AsUInt16());
(Vector256<uint> bottomRightLow, Vector256<uint> bottomRightHigh) = Vector256.Widen(bottomRight.AsUInt16());
Vector256<uint> rounding = Vector256.Create(2U);
Vector256<uint> low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + rounding) >> 2;
Vector256<uint> high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + rounding) >> 2;
return Vector256.Narrow(low, high).AsInt16();
}
/// <inheritdoc/>
public static Vector512<short> Filter(
Vector512<short> topLeft,
Vector512<short> topRight,
Vector512<short> bottomLeft,
Vector512<short> bottomRight)
{
(Vector512<uint> topLeftLow, Vector512<uint> topLeftHigh) = Vector512.Widen(topLeft.AsUInt16());
(Vector512<uint> topRightLow, Vector512<uint> topRightHigh) = Vector512.Widen(topRight.AsUInt16());
(Vector512<uint> bottomLeftLow, Vector512<uint> bottomLeftHigh) = Vector512.Widen(bottomLeft.AsUInt16());
(Vector512<uint> bottomRightLow, Vector512<uint> bottomRightHigh) = Vector512.Widen(bottomRight.AsUInt16());
Vector512<uint> rounding = Vector512.Create(2U);
Vector512<uint> low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + rounding) >> 2;
Vector512<uint> high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + rounding) >> 2;
return Vector512.Narrow(low, high).AsInt16();
}
}
}

231
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;
/// <summary>
/// Reconstructs AV1 intra-block-copy predictions from an earlier region of the current frame.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
internal static partial class Av1IntraBlockCopyPredictor
{
/// <summary>
/// Reconstructs an 8-bit intra-block-copy prediction.
/// </summary>
/// <param name="source">The source region beginning at the integer sample preceding any half-sample phase.</param>
/// <param name="sourceStride">The distance, in samples, between source rows.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
/// <param name="width">The prediction width in samples.</param>
/// <param name="height">The prediction height in samples.</param>
/// <param name="halfX">Indicates whether the horizontal source phase is one half-sample.</param>
/// <param name="halfY">Indicates whether the vertical source phase is one half-sample.</param>
public static void Predict(
ReadOnlySpan<byte> source,
int sourceStride,
Span<byte> 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<BilinearOperator>(source, sourceStride, destination, destinationStride, width, height);
}
else if (halfX)
{
Predict<HorizontalOperator>(source, sourceStride, destination, destinationStride, width, height);
}
else
{
Predict<VerticalOperator>(source, sourceStride, destination, destinationStride, width, height);
}
}
/// <summary>
/// Reconstructs a high-bit-depth intra-block-copy prediction.
/// </summary>
/// <param name="source">The source region beginning at the integer sample preceding any half-sample phase.</param>
/// <param name="sourceStride">The distance, in samples, between source rows.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
/// <param name="width">The prediction width in samples.</param>
/// <param name="height">The prediction height in samples.</param>
/// <param name="halfX">Indicates whether the horizontal source phase is one half-sample.</param>
/// <param name="halfY">Indicates whether the vertical source phase is one half-sample.</param>
public static void Predict(
ReadOnlySpan<short> source,
int sourceStride,
Span<short> 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<BilinearOperator>(source, sourceStride, destination, destinationStride, width, height);
}
else if (halfX)
{
Predict<HorizontalOperator>(source, sourceStride, destination, destinationStride, width, height);
}
else
{
Predict<VerticalOperator>(source, sourceStride, destination, destinationStride, width, height);
}
}
/// <summary>
/// Reconstructs an 8-bit intra-block-copy prediction without explicit hardware intrinsics.
/// </summary>
/// <param name="source">The source region beginning at the integer sample preceding any half-sample phase.</param>
/// <param name="sourceStride">The distance, in samples, between source rows.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
/// <param name="width">The prediction width in samples.</param>
/// <param name="height">The prediction height in samples.</param>
/// <param name="halfX">Indicates whether the horizontal source phase is one half-sample.</param>
/// <param name="halfY">Indicates whether the vertical source phase is one half-sample.</param>
public static void PredictScalar(
ReadOnlySpan<byte> source,
int sourceStride,
Span<byte> 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<BilinearOperator>(source, sourceStride, destination, destinationStride, width, height);
}
else if (halfX)
{
PredictScalar<HorizontalOperator>(source, sourceStride, destination, destinationStride, width, height);
}
else
{
PredictScalar<VerticalOperator>(source, sourceStride, destination, destinationStride, width, height);
}
}
/// <summary>
/// Reconstructs a high-bit-depth intra-block-copy prediction without explicit hardware intrinsics.
/// </summary>
/// <param name="source">The source region beginning at the integer sample preceding any half-sample phase.</param>
/// <param name="sourceStride">The distance, in samples, between source rows.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
/// <param name="width">The prediction width in samples.</param>
/// <param name="height">The prediction height in samples.</param>
/// <param name="halfX">Indicates whether the horizontal source phase is one half-sample.</param>
/// <param name="halfY">Indicates whether the vertical source phase is one half-sample.</param>
public static void PredictScalar(
ReadOnlySpan<short> source,
int sourceStride,
Span<short> 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<BilinearOperator>(source, sourceStride, destination, destinationStride, width, height);
}
else if (halfX)
{
PredictScalar<HorizontalOperator>(source, sourceStride, destination, destinationStride, width, height);
}
else
{
PredictScalar<VerticalOperator>(source, sourceStride, destination, destinationStride, width, height);
}
}
/// <summary>
/// Copies an 8-bit whole-sample source block to its destination.
/// </summary>
private static void Copy(ReadOnlySpan<byte> source, int sourceStride, Span<byte> 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));
}
}
/// <summary>
/// Copies a high-bit-depth whole-sample source block to its destination.
/// </summary>
private static void Copy(ReadOnlySpan<short> source, int sourceStride, Span<short> 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));
}
}
/// <summary>
/// Copies an 8-bit whole-sample source block with scalar sample assignments.
/// </summary>
private static void CopyScalar(ReadOnlySpan<byte> source, int sourceStride, Span<byte> 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];
}
}
}
/// <summary>
/// Copies a high-bit-depth whole-sample source block with scalar sample assignments.
/// </summary>
private static void CopyScalar(
ReadOnlySpan<short> source,
int sourceStride,
Span<short> 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];
}
}
}
}

8
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // 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;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -124,7 +125,12 @@ internal class Av1BlockModeInfo
/// <summary> /// <summary>
/// Gets or sets a value indicating whether intra block copy is selected. /// Gets or sets a value indicating whether intra block copy is selected.
/// </summary> /// </summary>
public bool UseUltraBlockCopy { get; set; } public bool UseIntraBlockCopy { get; set; }
/// <summary>
/// Gets or sets the intra-block-copy displacement vector in one-eighth-sample units.
/// </summary>
public Av1MotionVector DisplacementVector { get; set; }
/// <summary> /// <summary>
/// Gets or sets the packed chroma-from-luma alpha magnitude indices. /// Gets or sets the packed chroma-from-luma alpha magnitude indices.

29
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.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -214,6 +215,34 @@ internal class Av1PartitionInfo
} }
} }
/// <summary>
/// Gets the luma transform type covering a transform position from a subsampled chroma plane.
/// </summary>
/// <param name="planePosition">The transform position in 4x4 units of the target plane.</param>
/// <param name="subX">Indicates whether the target plane is horizontally subsampled.</param>
/// <param name="subY">Indicates whether the target plane is vertically subsampled.</param>
/// <returns>The transform type decoded at the corresponding luma-grid position.</returns>
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<Av1TransformInfo> 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.");
}
/// <summary> /// <summary>
/// Gets the block width clipped to the right frame edge. /// Gets the block width clipped to the right frame edge.
/// </summary> /// </summary>

62
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs

@ -4,6 +4,7 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; 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.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
@ -137,6 +138,16 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// </summary> /// </summary>
private readonly int[] coefficientIndex = []; private readonly int[] coefficientIndex = [];
/// <summary>
/// Reusable storage for the eight spatial displacement-vector candidates permitted by AV1.
/// </summary>
private InlineArray8<Av1MotionVector> displacementVectorCandidates;
/// <summary>
/// Reusable storage for the spatial weight associated with each displacement-vector candidate.
/// </summary>
private InlineArray8<int> displacementVectorWeights;
/// <summary> /// <summary>
/// Provides allocator and decoder configuration to tile entropy decoding. /// Provides allocator and decoder configuration to tile entropy decoding.
/// </summary> /// </summary>
@ -670,7 +681,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
partitionInfo.PopulateModeInfoNeighbors(this.SequenceHeader.ColorConfig); partitionInfo.PopulateModeInfoNeighbors(this.SequenceHeader.ColorConfig);
this.ReadModeInfo(ref reader, partitionInfo); this.ReadModeInfo(ref reader, partitionInfo, tileInfo);
this.ReadPaletteTokens(ref reader, partitionInfo); this.ReadPaletteTokens(ref reader, partitionInfo);
this.ReadBlockTransformSize(ref reader, modeInfoLocation, partitionInfo, superblockInfo, tileInfo); this.ReadBlockTransformSize(ref reader, modeInfoLocation, partitionInfo, superblockInfo, tileInfo);
if (partitionInfo.ModeInfo.Skip) if (partitionInfo.ModeInfo.Skip)
@ -994,8 +1005,29 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
Av1BlockSize planeBlockSize = partitionInfo.ModeInfo.BlockSize.GetSubsampled(subX, subY); Av1BlockSize planeBlockSize = partitionInfo.ModeInfo.BlockSize.GetSubsampled(subX, subY);
int blocksWide = partitionInfo.GetMaxBlockWide(planeBlockSize, subX); int blocksWide = partitionInfo.GetMaxBlockWide(planeBlockSize, subX);
int blocksHigh = partitionInfo.GetMaxBlockHigh(planeBlockSize, subY); int blocksHigh = partitionInfo.GetMaxBlockHigh(planeBlockSize, subY);
Av1TransformType lumaTransformType = partitionInfo.ModeInfo.UseIntraBlockCopy && plane > 0 && !isLossless
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); ? 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);
} }
/// <summary> /// <summary>
@ -1390,11 +1422,12 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// </summary> /// </summary>
/// <param name="reader">The tile symbol decoder.</param> /// <param name="reader">The tile symbol decoder.</param>
/// <param name="partitionInfo">The current coding block.</param> /// <param name="partitionInfo">The current coding block.</param>
/// <param name="tileInfo">The active tile boundaries.</param>
/// <remarks>Implements the intra-frame branch of AV1 section 5.11.6.</remarks> /// <remarks>Implements the intra-frame branch of AV1 section 5.11.6.</remarks>
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."); 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);
} }
/// <summary> /// <summary>
@ -1402,8 +1435,9 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// </summary> /// </summary>
/// <param name="reader">The tile symbol decoder.</param> /// <param name="reader">The tile symbol decoder.</param>
/// <param name="partitionInfo">The current coding block and its neighbors.</param> /// <param name="partitionInfo">The current coding block and its neighbors.</param>
/// <param name="tileInfo">The active tile boundaries.</param>
/// <remarks>Implements AV1 section 5.11.7.</remarks> /// <remarks>Implements AV1 section 5.11.7.</remarks>
private void ReadIntraFrameModeInfo(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo) private void ReadIntraFrameModeInfo(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo)
{ {
if (this.FrameHeader.SegmentationParameters.SegmentIdPrecedesSkip) if (this.FrameHeader.SegmentationParameters.SegmentIdPrecedesSkip)
{ {
@ -1436,8 +1470,24 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
if (useIntraBlockCopy) if (useIntraBlockCopy)
{ {
partitionInfo.ModeInfo.UseIntraBlockCopy = true;
partitionInfo.ModeInfo.YMode = Av1PredictionMode.DC; partitionInfo.ModeInfo.YMode = Av1PredictionMode.DC;
partitionInfo.ModeInfo.UvMode = 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 else
{ {

117
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.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; 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 // Intra-block copy is signaled on an intra-only frame but follows AV1's inter prediction and transform
// predicts its samples before any coded residual is added. // rules. Its validated displacement always references an earlier reconstructed region of this frame.
if (highBitDepth) if (modeInfo.UseIntraBlockCopy)
{ {
this.predictionDecoder.Decode( // libaom predicts the complete coding block before traversing its residual transforms. The mandatory
partitionInfo, // 256-pixel source delay prevents overlap, and the two-tap interpolation is translation-invariant,
(Av1Plane)plane, // so predicting the matching source rectangle for each transform unit produces the same samples.
transformSize, Point transformPixelPosition = new(
tileInfo, pixelPosition.X + (transformInfo[0].OffsetX << Av1Constants.ModeInfoSizeLog2),
highBitDepthTransformBlockReconstructionBuffer, pixelPosition.Y + (transformInfo[0].OffsetY << Av1Constants.ModeInfoSizeLog2));
reconstructionStride,
this.frameBuffer.BitDepth, // Displacement vectors use one-eighth luma-sample units. Converting them to the plane's q4 grid
transformInfo[0].OffsetX, // leaves luma on an integer sample and can leave subsampled chroma exactly at phase eight.
transformInfo[0].OffsetY); 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<short> 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<byte> 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 else
{ {
this.predictionDecoder.Decode( // Conventional intra prediction consumes the reference-prefixed destination span before the
partitionInfo, // transform residual is reconstructed over its first output row.
(Av1Plane)plane, if (highBitDepth)
transformSize, {
tileInfo, this.predictionDecoder.Decode(
transformBlockReconstructionBuffer, partitionInfo,
reconstructionStride, (Av1Plane)plane,
this.frameBuffer.BitDepth, transformSize,
transformInfo[0].OffsetX, tileInfo,
transformInfo[0].OffsetY); 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; int numberOfCoefficients = 0;

15
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs

@ -44,7 +44,10 @@ public class Av1CoefficientsEntropyTests
using IMemoryOwner<byte> encoded = encoder.Exit(); using IMemoryOwner<byte> encoded = encoder.Exit();
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); 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
Assert.Equal(endOfBlock, actuals[0]); Assert.Equal(endOfBlock, actuals[0]);
@ -94,7 +97,10 @@ public class Av1CoefficientsEntropyTests
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex);
int plane = Math.Min((int)componentType, 1); 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
Assert.Equal(endOfBlock, actuals[0]); Assert.Equal(endOfBlock, actuals[0]);
@ -148,7 +154,10 @@ public class Av1CoefficientsEntropyTests
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex);
int plane = Math.Min((int)componentType, 1); 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
Assert.Equal(endOfBlock, actuals[0]); Assert.Equal(endOfBlock, actuals[0]);

47
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs

@ -4,6 +4,7 @@
using System.Buffers; using System.Buffers;
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; 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.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
@ -373,7 +374,7 @@ public class Av1EntropyTests
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex);
for (int i = 0; i < values.Length; i++) 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 // Assert
@ -557,6 +558,50 @@ public class Av1EntropyTests
Assert.Equal(values, actuals); Assert.Equal(values, actuals);
} }
/// <summary>
/// Verifies integer displacement-vector joints, signs, magnitude classes, and adaptive offset bits.
/// </summary>
[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<byte> 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<int> GetRangeData(int count) public static TheoryData<int> GetRangeData(int count)
{ {
TheoryData<int> result = []; TheoryData<int> result = [];

235
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;
/// <summary>
/// Verifies AV1 intra-block-copy interpolation across the supported hardware-intrinsic configurations.
/// </summary>
[Trait("Format", "Heif")]
public class Av1IntraBlockCopyPredictorTests
{
/// <summary>
/// Exercises each SIMD register-width tier and the complete scalar fallback.
/// </summary>
private const HwIntrinsics PredictorConfigurations =
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic;
/// <summary>
/// Verifies all four source phases for 8-bit samples at every AV1 transform size.
/// </summary>
[Fact]
public void EightBitPredictionMatchesScalarAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateEightBitPrediction, PredictorConfigurations);
/// <summary>
/// Verifies all four source phases for high-bit-depth samples at every AV1 transform size.
/// </summary>
[Fact]
public void HighBitDepthPredictionMatchesScalarAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthPrediction, PredictorConfigurations);
/// <summary>
/// Verifies the four normative interpolation equations against independently calculated sample blocks.
/// </summary>
[Fact]
public void PredictionMatchesKnownInterpolationValues()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateKnownInterpolationValues, PredictorConfigurations);
/// <summary>
/// Compares the SIMD-first 8-bit implementation with its scalar definition and verifies that row padding is unchanged.
/// </summary>
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);
}
}
}
/// <summary>
/// Compares the SIMD-first high-bit-depth implementation with its scalar definition and verifies exact-width stores.
/// </summary>
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);
}
}
}
/// <summary>
/// Applies each source phase to a four-by-four block whose expected results are simple arithmetic progressions.
/// </summary>
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<byte> copied =
[
0, 4, 8, 12,
20, 24, 28, 32,
40, 44, 48, 52,
60, 64, 68, 72,
];
ReadOnlySpan<byte> horizontal =
[
2, 6, 10, 14,
22, 26, 30, 34,
42, 46, 50, 54,
62, 66, 70, 74,
];
ReadOnlySpan<byte> vertical =
[
10, 14, 18, 22,
30, 34, 38, 42,
50, 54, 58, 62,
70, 74, 78, 82,
];
ReadOnlySpan<byte> 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);
}
/// <summary>
/// Verifies one four-by-four source phase for both 8-bit and translated high-bit-depth samples.
/// </summary>
/// <param name="source">The five-by-five 8-bit source region.</param>
/// <param name="sourceStride">The number of source samples per row.</param>
/// <param name="expected">The independently calculated four-by-four prediction.</param>
/// <param name="halfX">Indicates whether the horizontal phase is one half-sample.</param>
/// <param name="halfY">Indicates whether the vertical phase is one half-sample.</param>
private static void ValidateKnownPhase(
ReadOnlySpan<byte> source,
int sourceStride,
ReadOnlySpan<byte> 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);
}
}

149
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;
/// <summary>
/// Verifies AV1 intra-block-copy reference derivation and displacement-vector legality rules.
/// </summary>
[Trait("Format", "Heif")]
public class Av1IntraBlockCopyTests
{
/// <summary>
/// Verifies the horizontal fallback used in the tile's first superblock row.
/// </summary>
[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);
}
/// <summary>
/// Verifies the vertical fallback used after the tile's first superblock row when spatial candidates are absent.
/// </summary>
[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);
}
/// <summary>
/// Verifies tile bounds, whole-sample precision, the four-block delay, and wavefront ordering.
/// </summary>
[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));
}
/// <summary>
/// Creates the 640-by-256, 4:2:0 sequence geometry shared by the displacement tests.
/// </summary>
private static ObuSequenceHeader CreateSequenceHeader()
=> new()
{
MaxFrameWidth = 640,
MaxFrameHeight = 256,
Use128x128Superblock = false,
ColorConfig = new ObuColorConfig
{
IsMonochrome = false,
SubSamplingX = true,
SubSamplingY = true,
BitDepth = Av1BitDepth.EightBit,
},
};
/// <summary>
/// Creates one tile covering the complete test frame.
/// </summary>
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);
}
}

4
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SymbolContextTests.cs

@ -38,8 +38,8 @@ public class Av1SymbolContextTests
Av1TransformSetType transformSetType = (Av1TransformSetType)setType; Av1TransformSetType transformSetType = (Av1TransformSetType)setType;
// Act // Act
Av1TransformType transformType = Av1SymbolContextHelper.ExtendedTransformInverse[(int)transformSetType][index]; Av1TransformType transformType = Av1SymbolContextHelper.GetExtendedTransformType(transformSetType, index);
int actualIndex = Av1SymbolContextHelper.ExtendedTransformIndices[(int)transformSetType][(int)transformType]; int actualIndex = Av1SymbolContextHelper.GetExtendedTransformIndex(transformSetType, transformType);
// Assert // Assert
Assert.Equal(actualIndex, index); Assert.Equal(actualIndex, index);

Loading…
Cancel
Save