Browse Source

Implement layered AV1 decoder state

pull/2633/head
James Jackson-South 1 week ago
parent
commit
c19915ab11
  1. 29
      HEIF_IMPLEMENTATION_PLAN.md
  2. 87
      src/ImageSharp/Formats/Heif/Av1/Av1BitStreamReader.cs
  3. 342
      src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs
  4. 58
      src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs
  5. 85
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs
  6. 52
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DisplacementVectorContext.cs
  7. 25
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs
  8. 519
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContext.cs
  9. 150
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContexts.cs
  10. 8
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolContextHelper.cs
  11. 325
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs
  12. 74
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs
  13. 47
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolReader.cs
  14. 15
      src/ImageSharp/Formats/Heif/Av1/IAv1TileReader.cs
  15. 234
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionParameters.cs
  16. 30
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionType.cs
  17. 26
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopy.cs
  18. 235
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFilmGrainParameters.cs
  19. 172
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs
  20. 101
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameReferenceState.cs
  21. 12
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameSize.cs
  22. 8
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOrderHintInfo.cs
  23. 797
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs
  24. 20
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSegmentationParameters.cs
  25. 9
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs
  26. 12
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs
  27. 35
      src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainDecoder.cs
  28. 2
      src/ImageSharp/Formats/Heif/Av1/Pipeline/IAv1FrameDecoder.cs
  29. 90
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1ChromaPredictionMode.cs
  30. 46
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1ChromaPredictionModeExtensions.cs
  31. 16
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.Operator.cs
  32. 25
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.OperatorContract.cs
  33. 174
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.Operator.cs
  34. 183
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.OperatorContract.cs
  35. 66
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs
  36. 100
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionMode.cs
  37. 728
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Dispatch.cs
  38. 419
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Operator.cs
  39. 16
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.cs
  40. 17
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterpolationFilter.cs
  41. 137
      src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.BilinearOperator.cs
  42. 78
      src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.HorizontalOperator.cs
  43. 135
      src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.Operator.cs
  44. 136
      src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.OperatorContract.cs
  45. 75
      src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.VerticalOperator.cs
  46. 135
      src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrame.cs
  47. 111
      src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameBorder.cs
  48. 279
      src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameDerivation.cs
  49. 265
      src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameStore.cs
  50. 124
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs
  51. 30
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1CompoundType.cs
  52. 20
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1DifferenceWeightedMaskType.cs
  53. 4
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderBlockModeInfo.cs
  54. 536
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.MotionField.cs
  55. 117
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs
  56. 30
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1InterIntraMode.cs
  57. 25
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1MotionMode.cs
  58. 59
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionInfo.cs
  59. 55
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ReferenceFrameType.cs
  60. 436
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs
  61. 14
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs
  62. 47
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs
  63. 29
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Adst16Operator.cs
  64. 29
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Adst4Operator.cs
  65. 29
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Adst8Operator.cs
  66. 29
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Dct16Operator.cs
  67. 29
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Dct32Operator.cs
  68. 29
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Dct4Operator.cs
  69. 29
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Dct64Operator.cs
  70. 29
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Dct8Operator.cs
  71. 29
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Identity16Operator.cs
  72. 29
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Identity32Operator.cs
  73. 29
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Identity4Operator.cs
  74. 29
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Identity8Operator.cs
  75. 80
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Operator.cs
  76. 574
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Adst16Operator.cs
  77. 147
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Adst4Operator.cs
  78. 295
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Adst8Operator.cs
  79. 481
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Dct16Operator.cs
  80. 1033
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Dct32Operator.cs
  81. 118
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Dct4Operator.cs
  82. 2278
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Dct64Operator.cs
  83. 238
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Dct8Operator.cs
  84. 71
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Identity16Operator.cs
  85. 71
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Identity32Operator.cs
  86. 71
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Identity4Operator.cs
  87. 71
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Identity8Operator.cs
  88. 166
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Operator.cs
  89. 100
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.OutputOperator.cs
  90. 94
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformOutputOperator.cs
  91. 12
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseWalshHadamardTransformer.cs
  92. 21
      src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1Adst16Forward1dOperator.cs
  93. 21
      src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1Adst4Forward1dOperator.cs
  94. 21
      src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1Adst8Forward1dOperator.cs
  95. 21
      src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1Dct16Forward1dOperator.cs
  96. 21
      src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1Dct32Forward1dOperator.cs
  97. 21
      src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1Dct4Forward1dOperator.cs
  98. 21
      src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1Dct64Forward1dOperator.cs
  99. 21
      src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1Dct8Forward1dOperator.cs
  100. 21
      src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1Identity16Forward1dOperator.cs

29
HEIF_IMPLEMENTATION_PLAN.md

@ -29,7 +29,7 @@ Checkboxes may be marked complete only when the implementation and the verificat
## Delivery dashboard
Last reconciled with the source tree on 2026-08-27 against the worktree based on commit `32d5e3b51`, including the completed AV1 transform, OBU-framing, intra-block-copy, 12-profile reconstruction, and layered-item property checkpoints. 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 `f6d3da2b31`, including the completed AV1 transform, OBU-framing, intra-block-copy, 12-profile reconstruction, layered-item property, and layered reference/header-state checkpoints. 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:
@ -40,13 +40,13 @@ Status meanings:
Current development stage: **Stage 3 — complete AV1 still-image decoding.** The transform checkpoint is closed: forward transforms use one libaom-shaped SIMD-first operator architecture across `Vector512`, `Vector256`, and `Vector128`, with scalar fallback; inverse production traversal uses the verified `Vector256` and `Vector128` tiers with scalar fallback; and implementation-mechanic type and file suffixes have been removed. Neither AV1 nor HEVC production encoding is implemented.
Immediate checkpoint: **complete layered AV1 image-item decoding through the existing image-only container surface.** This includes `a1op`, `lsel`, and `a1lx` properties, operating-point selection, dependency-preserving layer consumption, and final or explicitly selected spatial-layer output for color, alpha, and grid items. It requires stateful AV1 reference/CDF reconstruction; it must not be represented as animation or expanded into a general ISO BMFF/video model.
Immediate checkpoint: **complete layered AV1 image-item decoding through the existing image-only container surface.** This includes `a1op`, `lsel`, and `a1lx` properties, operating-point selection, dependency-preserving layer consumption, and final or explicitly selected spatial-layer output for color, alpha, and grid items. The bounded decoder session now retains reference owners and the header, entropy, segmentation, loop-filter, global-motion, and temporal motion-field state required by dependent layers. Inter tile syntax and reconstruction still need to consume that state. This work must not be represented as animation or expanded into a general ISO BMFF/video model.
| Order | Delivery stage | State | Delivered state | Gate that remains open |
| --- | --- | --- | --- | --- |
| 1 | Baseline, provenance, documentation, and public contract | In progress | Pinned codec references, a bounded image-only scope, encoder options, typed bit depth, decoder-option propagation, and extensive HEIF documentation exist. | Complete the all-file documentation audit, record a fresh Release baseline, finish distinct public HEIC/AVIF save boundaries, and close API review. |
| 2 | Bounded HEIF item and image-sequence container | In progress | Still-item parsing, grids, auxiliary alpha, metadata properties, bounded image-sequence tracks, Identify, and all-sync AV1 sequence presentation are connected. | Complete adversarial boundary coverage, remaining item/property behavior, reference-dependent sequence reconstruction, and the bounded sequence writer. |
| 3 | Still-image AV1 and HEVC decoding | **Current** | HEVC reconstruction reaches exact HM/libheif fixtures across the recorded 8/10/12-bit and chroma cases. AV1 includes bounded OBU framing, reconstruction, filters, grain, color, transforms, intra-block copy, and an exact independent 12-profile bit-depth/chroma matrix through every dispatch tier. | Remove every remaining valid AV1 still-image unsupported branch with independent compression-tool vectors, then complete the remaining HEVC profile and Range Extensions 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, intra-block copy, an exact independent 12-profile bit-depth/chroma matrix through every dispatch tier, and retained layered reference/header/CDF state. | Complete inter tile syntax and reconstruction, including temporal segmentation consumption and motion/global/warped prediction, remove every other valid AV1 still-image unsupported branch with independent compression-tool vectors, then complete the remaining HEVC profile and Range Extensions matrix. |
| 4 | Complete decoded presentation and animation | In progress | Shared SIMD-first AV1/HEVC color conversion, ICC application, grids, transforms, direct planar alpha composition, frame metadata, repetition, and independently decodable AV1 sequence samples exist. | Close the full color/ICC cross-product, HEVC sequence decoding, AV1/HEVC reference-dependent samples, frame-local metadata/alpha behavior, and independent animated decode vectors. |
| 5 | AV1/AVIF encoding | Not started | RGB-to-planar conversion, forward transforms, OBU writer foundations, options, and container-writing infrastructure exist. | `HeifEncoderCore` still rejects AV1. Implement a real independently decodable lossy/lossless AV1 payload and the complete AVIF item/metadata matrix. |
| 6 | HEVC/HEIC encoding | Not started | Shared input color conversion, options, and HEIF writer infrastructure exist. | `HeifEncoderCore` still rejects HEVC. Implement a real independently decodable lossy/lossless HEVC payload and the complete HEIC item/metadata matrix. |
@ -63,10 +63,16 @@ Immediate checkpoint: **complete layered AV1 image-item decoding through the exi
- [x] Validate `a1lx` layer boundaries against the logical item size and restrict concrete `lsel` decoding to the cumulative payload through the selected spatial layer without copying item bytes.
- [x] Apply the selected `a1op` operating-point mask while consuming extended OBUs and validate the selected index against the parsed sequence header.
- [x] Store the eight fixed reference-validity, order-hint, and map-index tables inline on the frame header, retaining complete multi-bit order hints without per-header array allocations.
- [ ] Preserve reconstruction, reference-frame, primary-CDF, segmentation, loop-filter, and motion state across every dependent layer in one image-item decoder session.
- [x] Enforce the bounded frame lifecycle across temporal delimiters, frame headers, tile coverage, and payload completion, publishing parser reference state only after successful decoder completion. The real two-layer libavif-derived lifecycle fixture verifies frame completion, retained-slot occupancy, reference resolution, and inherited dimensions through a fake tile lifecycle; it does not verify inter reconstruction or pixels.
- [x] Retain each successfully reconstructed reference as the sole owner of its ungrained sample planes, frame header, frame information, and published entropy snapshot; refresh only the selected slots, extend reference borders once after in-loop filtering, preserve a separate grained presentation buffer when required, and reset the store at an accepted sequence boundary.
- [x] Parse full and short reference signaling against retained-slot occupancy separately from frame-ID validity, resolve the primary-reference slot, implement `frame_size_with_refs`, and read high-precision-motion-vector, interpolation-filter, switchable-motion-mode, and reference-frame-motion-vector flags.
- [x] Initialize each frame's working CDF graph from the resolved primary snapshot, inherit segmentation feature data and an unchanged contiguous segmentation map, inherit loop-filter delta state, parse and inherit global-motion parameters, and initialize/project the retained per-8x8 temporal motion field.
- [x] Match libaom's bounded range-decoder consumed-bit accounting and tile trailing-bit validation so implicit zero refill cannot conceal truncated entropy data. Validate before publishing the selected CDF, reset decoder state after failure, and unwind every successful frame/block workspace rent when a later constructor allocation fails. A real truncated palette tile, decoder reuse, parser lifecycle, and allocator-identity tests cover these boundaries.
- [ ] Consume the retained segmentation map through `seg_id_predicted` when temporal segmentation is enabled. The current intra-only tile path does not read that inter-block decision.
- [ ] Implement the complete inter-frame entropy, mode, motion-vector, compound-prediction, inter-prediction, and warped/global-motion paths permitted by the image profile.
- [x] Implement allocation-free SIMD-first translational single-reference interpolation for regular, smooth, sharp, and bilinear filters across 8/10/12-bit samples. The predictor mirrors JPEG's closed static operator architecture, descends through `Vector512`, `Vector256`, and `Vector128` before scalar fallback, and passes the exact independent convolution oracle through `FeatureTestRunner`.
- [ ] Connect reference selection, motion-vector derivation, compound modes, warped/global motion, and reconstructed-plane writes through tile decoding, then verify them with independently encoded inter-frame AV1 image-layer fixtures.
- [ ] Decode inter-block reference selection, motion-vector derivation, compound and inter-intra modes, masked blending, OBMC, and warped/global-motion prediction, then write predicted and residual samples through tile reconstruction.
- [ ] Verify every connected mode and filter with independently encoded dependent-layer AV1 image-item fixtures and exact native-plane comparisons.
- [ ] Return the explicitly selected spatial layer or the final displayed layer, keeping reference reconstruction separate from display-only film grain.
- [ ] Verify color and auxiliary-alpha output exactly against both pinned libavif progressive fixtures under normal SIMD dispatch and all required `FeatureTestRunner` fallbacks.
- [ ] Correct the audited 12-bit inverse ADST4, Identity4, and Identity16 SIMD arithmetic by widening only the libaom-widened multiply/accumulate operations, with exact conformant-range vectors and `FeatureTestRunner` coverage.
@ -289,7 +295,7 @@ This snapshot pins or classifies the available references and failures; it does
| `Av1WienerFilter` | AV1 sections 7.17.4 and 7.17.5 Wiener restoration filtering and coefficient derivation | libaom `av1/common/restoration.c`, `av1/common/restoration.h`, `av1/common/convolve.c`, and `av1/common/convolve.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve the implicit center-sample contribution, separable horizontal/vertical rounding, bit-depth-dependent 16-bit intermediate range, and final 8/10/12-bit clipping. Reuse `Vector128_.MultiplyAddAdjacent` for the contiguous horizontal eight-tap product with an exact scalar fallback. Keep the restoration stage disabled until stripe boundaries and self-guided filtering are both complete. |
| `Av1SelfGuidedFilter` | AV1 sections 7.17.2 and 7.17.3 self-guided and box-filter processes | libaom `av1/common/restoration.c` and `av1/common/restoration.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Design the window layout, caller-owned scratch, local statistics, and projection traversal from libaom's architecture-specific kernels so the production path is SIMD-first. Implement the equivalent scalar fallback through the same contract. Preserve the sixteen normative radius/variance parameter sets, local mean and variance normalization, alternating-row radius-two optimization, decoded projection-coefficient behavior, signed rounding, and 8/10/12-bit clipping. Keep this image-reconstruction stage disabled until restoration stripe boundaries are complete. |
| `Av1LoopRestorationBoundary`, `Av1LoopRestorationDecoder`, and `Av1FrameDecoder` restoration-stage ordering | AV1 section 7.17 loop restoration, including striped boundary semantics | libaom `av1/common/restoration.c`, `av1/common/restoration.h`, `av1/common/resize.c`, and `av1/decoder/decodeframe.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve two deblocked rows at internal 64-luma stripe boundaries before CDEF, apply the existing normative SIMD-backed super-resolution kernel to saved rows when scaled, use post-CDEF/super-resolution samples at frame edges, extend the final restoration unit up to 150 percent of nominal size, and filter from immutable plane snapshots into separate output planes. This is bounded still-image reconstruction state, not retained reference-frame, track, timing, or playback state. |
| `Av1FilmGrainDecoder`, `Av1FilmGrainGaussianSequence`, `Av1FilmGrainNoise`, `Av1FilmGrainOverlap`, and `Av1FilmGrainSampleOperator<TSample>` | AV1 section 7.18 film-grain synthesis | libaom `av1/decoder/grain_synthesis.c`, `av1/decoder/grain_synthesis.h`, and `aom_dsp/grain_params.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve the normative 2,048-sample Gaussian sequence as compile-time span data, the serial linear-feedback shift register and luma/chroma autoregressive templates, scaling lookup interpolation, 32x32 block selection, boundary overlap, restricted-range clipping, monochrome and 4:2:0/4:2:2/4:4:4 layouts, and 8/10/12-bit arithmetic. Use allocator-owned scratch. Apply scaling and noise through AVX2 gather arithmetic at every bit depth, retain the measured cross-platform 128-bit high-bit-depth path, and use the exact scalar path for 8-bit machines without AVX2 and for vector tails. Process horizontal overlap through preferred-native 512-bit, then 256-bit, 128-bit, and scalar tiers; retain scalar vertical overlap because each output row exposes only one or two strided samples. Keep SIMD width, ISA, sample storage, and bit depth out of folders, namespaces, files, and type names. Apply grain only to displayed still-image samples after all in-loop filters; reference-frame parameter inheritance remains sequence-playback state and is outside this codec scope. |
| `Av1FilmGrainDecoder`, `Av1FilmGrainGaussianSequence`, `Av1FilmGrainNoise`, `Av1FilmGrainOverlap`, and `Av1FilmGrainSampleOperator<TSample>` | AV1 section 7.18 film-grain synthesis | libaom `av1/decoder/grain_synthesis.c`, `av1/decoder/grain_synthesis.h`, and `aom_dsp/grain_params.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve the normative 2,048-sample Gaussian sequence as compile-time span data, the serial linear-feedback shift register and luma/chroma autoregressive templates, scaling lookup interpolation, 32x32 block selection, boundary overlap, restricted-range clipping, monochrome and 4:2:0/4:2:2/4:4:4 layouts, and 8/10/12-bit arithmetic. Use allocator-owned scratch. Apply scaling and noise through AVX2 gather arithmetic at every bit depth, retain the measured cross-platform 128-bit high-bit-depth path, and use the exact scalar path for 8-bit machines without AVX2 and for vector tails. Process horizontal overlap through preferred-native 512-bit, then 256-bit, 128-bit, and scalar tiers; retain scalar vertical overlap because each output row exposes only one or two strided samples. Keep SIMD width, ISA, sample storage, and bit depth out of folders, namespaces, files, and type names. Apply grain only to displayed samples after all in-loop filters, inherit reference-selected parameters through the bounded image-layer decoder state, and keep the ungrained reconstruction in every refreshed reference slot. This does not add sequence playback or video scope. |
| `Av1FrameInfo`, `Av1TileReader`, and `Av1BlockDecoder` transform/coefficient storage | AV1 section 5.11.39 coefficient syntax and section 7.11.2 reconstruction | libaom `av1/decoder/decodetxb.c` and `av1/decoder/decoder.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve separate luma and chroma transform coefficients at monotonically advancing per-plane offsets within each superblock so reconstruction consumes the same transform-block order produced by tile parsing. |
| `Av1InverseQuantizer` and `Av1InverseQuantizationLookup` | AV1 section 7.12.3 inverse quantization | libaom `aom_dsp/aom_dsp_common.h`, `av1/common/quant_common.c`, and `av1/decoder/decodetxb.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Select the per-segment matrix level, alias 64-pixel transform dimensions to their adjusted matrices, retain a flat level-15 matrix, and apply the five-bit inverse-matrix weight scale. The large managed lookup remains a single process-wide table. |
| `Av1ForwardTransformer`, `Av1Inverse2dTransformer`, `Av1Transform2dFlipConfiguration`, the forward/inverse 1-D operator structs, and `Av1Transform1dMath` | AV1 forward transform definitions and section 7.11.2 inverse transform and reconstruction | libaom `av1/encoder/av1_fwd_txfm2d_hwy.h`, its AVX-512 and AVX2 instantiations, `av1/encoder/av1_fwd_txfm1d.c`, `av1/common/av1_inv_txfm1d_cfg.h`, `av1/common/av1_inv_txfm1d.c`, `av1/common/av1_inv_txfm2d.c`, the x86 AVX2/SSE4 implementations, and the corresponding Neon implementations at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve the normative staged DCT, ADST, and identity arithmetic, direction-specific stage ranges and shifts, transposition, clipping, and high-bit-depth sample addition. Stateless static-generic operators follow ImageSharp's JPEG color-transform pattern. Forward operators share one behavior model across scalar, `Vector128`, `Vector256`, and `Vector512`, selecting packed or expanded lane storage at the 2-D boundary. Inverse production traversal retains the verified scalar, `Vector128`, and `Vector256` tiers until a wider upstream shape and complete-block benefit are both established. |
@ -364,7 +370,7 @@ This assessment was reconciled with the source tree on 2026-08-26. Unless a resu
### AV1 decoder
- The single-still `Av1Decoder` path now parses tile state before allocating and reconstructing one independently decodable frame, and it disposes the reconstruction planes after pixel conversion. It deliberately does not retain animation/video reference frames or implement `show_existing_frame` playback state.
- The bounded `Av1Decoder` session parses all tile state before allocating and reconstructing each coded image layer. After successful completion it retains the ungrained reference planes, frame header, frame information, and published entropy snapshot in the refreshed slots, while presentation-only ownership remains separate. A new accepted sequence header resets both parser and retained-owner state. This is dependency reconstruction within one bounded image item; `show_existing_frame` playback remains rejected and no animation/video reference model is exposed.
- Transform coefficient entropy derivation and updates now address the above contexts relative to the tile column and the left contexts relative to the current superblock row, preserve luma coordinates independently of chroma subsampling, and test every packed context entry for the libaom any-nonzero rule. Extended vertical partition updates advance the mode-information column rather than the row. The existing multi-superblock 4:4:4 AVIF fixture now completes tile parsing; independent coefficient-context vectors across tile boundaries, chroma layouts, bit depths, and edge-clipped transforms remain required.
- The reconstruction pipeline now records plane-relative transform geometry, preserves tile-local delta-Q and delta-LF predictors, derives segmentation and reference-adjusted filter levels, and runs the exact AV1 4-, 6-, 8-, and 14-tap deblocking kernels in normative vertical-then-horizontal order. Deblocking uses the same closed edge-operator architecture as the HEVC filter, with operators specialized by sample storage and orientation, `Vector128<int>` lanes representing the four rows or columns along an edge, and an allocation-free scalar fallback for disabled intrinsics. Exact native-plane comparison with pinned scalar libaom output now verifies active deblocking and complete reconstruction for real 8-bit 4:2:0, 10-bit 4:4:4, and 12-bit 4:4:4 content; genuine AVIF containers separately verify presentation and public bit-depth metadata. The pipeline then applies CDEF through one semantic filter architecture: paired AVX2 and single-block `Vector128` direction analysis, closed primary/secondary strength operators, packed 4x4/4x8/8x4/8x8 constrained filtering, byte/16-bit output operators, and an exact allocation-free scalar fallback. Decoder orchestration now owns the immutable plane snapshots and clean direction/variance maps through ImageSharp's memory allocator, widens 8-bit source rows with the same AVX2/128-bit/scalar tiers as libaom, lists each unit's non-skipped blocks in fixed inline storage, analyzes listed blocks in pairs, and writes filtered bytes or 16-bit samples directly to the frame planes. Independently encoded active-CDEF 8-bit 4:2:0 and 10/12-bit 4:4:4 streams match every visible native sample produced by pinned scalar libaom with restoration disabled. Independently encoded AVIF containers at the same three bit depths also match pinned scalar-libavif presentation exactly under normal, 256-bit, 128-bit, and scalar color-conversion dispatch. Active super-resolution derives the Appendix A bounded coded width and applies the exact 64-phase, 8-tap horizontal filter with aligned reconstruction-edge input, 8/10/12-bit clipping, and the existing cross-platform `Vector128_.MultiplyAddAdjacent` helper. Independently encoded active-super-resolution AV1 streams at 8, 10, and 12 bits match every visible native sample produced by pinned scalar libaom under normal and forced-scalar dispatch. Independently packaged AVIF containers at the same bit depths retain matching libavif profile, dimensions, chroma, and CICP properties, require active super-resolution in their actual AV1 item, and match pinned scalar-libavif presentation exactly under normal, 256-bit, 128-bit, and scalar color-conversion dispatch. Loop restoration follows super-resolution, preserves the required pre-CDEF deblocked context at internal stripes, and applies decoded Wiener or self-guided units from immutable plane snapshots. Independently encoded active-restoration streams at 8, 10, and 12 bits now match every native sample from pinned scalar libaom across AVX2, 128-bit, and scalar dispatch, with the fixture matrix proving both Wiener and self-guided unit selection. The matching AVIF containers also match pinned scalar-libavif presentation exactly, and an independent direct-window definition verifies all sixteen self-guided parameter sets at each supported bit depth across vector and scalar dispatch. Combined 8-bit 4:2:0, 10-bit 4:2:2, and 12-bit 4:4:4 streams additionally verify restoration-unit boundaries after super-resolution, including clipped chroma transform traversal at a coded-frame edge.
- The visible still-image path applies the complete self-contained film-grain parameter set after all in-loop filters. Independently encoded pinned-libaom vectors match every native sample at 8, 10, and 12 bits across monochrome, 4:2:0, 4:2:2, and 4:4:4 layouts, full and restricted ranges, identity-matrix signaling, overlap, and odd 33x11 frame extension. `FeatureTestRunner` verifies normal, AVX-disabled, and fully scalar dispatch. A full-HD-equivalent 4:2:0 benchmark reports zero allocation: 8-bit AVX2 is 2.335 ms versus 5.806 ms scalar, while 12-bit AVX2 is 3.195 ms, cross-platform 128-bit is 7.382 ms, and scalar is 8.614 ms on the measured Ryzen platform. The slower 8-bit 128-bit lookup construction is deliberately not dispatched.
@ -393,7 +399,7 @@ This assessment was reconciled with the source tree on 2026-08-26. Unless a resu
### Tests
- HEVC coverage includes exact native-plane comparison with HM output, exact complete-image comparison with pinned libheif/libde265 references, and the 10 official Sony GENERAL Range Extensions first-picture fixtures across 8/10/12-bit monochrome, 4:2:0, 4:2:2, and 4:4:4 reconstruction. The remaining exposed profiles and individual Range Extensions tools still need exact independent vectors.
- AV1 has focused bitstream, prediction, entropy, reconstruction, filter, film-grain, color, and transform coverage, plus real libavif inputs. Valid still-image syntax paths still contain explicit unsupported branches, so the independent AV1 decode matrix is not complete.
- AV1 has focused bitstream, prediction, entropy, reconstruction, filter, film-grain, color, and transform coverage, plus real libavif inputs. A real two-layer libavif-derived OBU stream verifies the bounded frame lifecycle, retained-slot occupancy, resolved inter references, and `frame_size_with_refs` dimensions through a fake tile lifecycle. A real palette stream truncated inside its tile entropy payload verifies libaom-equivalent overflow/trailing-bit rejection and decoder-session recovery. The current Release checkpoint passes all 2,422 selected entropy, ownership, reference, predictor, intra-block-copy, and transform cases. It does not decode inter tile syntax or compare dependent-layer reconstructed pixels. Valid still-image syntax paths still contain explicit unsupported branches, so the independent AV1 decode matrix is not complete.
- The AV1 transform matrix verifies scalar, `Vector128`, `Vector256`, and `Vector512` forward representations plus the production inverse tiers across every valid size/type combination and supported bit depth. All 511 focused forward and inverse cases pass in Release; `FeatureTestRunner` isolates hardware tiers, every two-dimensional configuration exercises production dispatch, and the complete-block benchmark records zero managed allocation.
- Independent libavif fixtures cover primary, grid, auxiliary-alpha, ICC, metadata-skipping, and all-sync image-sequence presentation. Reference-dependent AV1 and HEVC sequence reconstruction and independent HEVC ICC sequence coverage remain open.
- Focused decoder-option tests cover strict, ancillary-only, image-data, and metadata-skipping behavior for still items and sequence samples. Complete adversarial dimension, allocation, malformed-container, and resource-limit coverage remains open.
@ -511,7 +517,7 @@ Implement and verify in dependency order:
- [ ] OBU framing, sequence headers, frame headers, tile groups, byte alignment, and trailing bits.
- [x] Isolate every declared OBU payload with a zero-copy bounded span reader, advance ignored metadata and reserved units without parsing their bytes as headers, validate padding and trailing bytes, enforce primary/redundant/combined frame-header order, accept the final unsized low-overhead OBU by consuming the bounded image-item remainder, and reject lengths that cross the containing boundary. Focused malformed-input tests and independent 8/10/12-bit AVIF reconstruction pass in Release.
- [ ] One coherent decoder lifecycle that retains parsed frame and tile state and disposes all buffers deterministically.
- [x] One coherent bounded decoder lifecycle retains parsed tile state until successful frame completion, commits frame-owned reference samples and metadata only after reconstruction, resets retained state at a new sequence boundary, and disposes tile, reference, entropy, and presentation ownership deterministically. The real two-layer lifecycle fixture proves the header/reference ownership boundary through a fake tile reader; dependent inter reconstruction remains covered by the unchecked items below.
- [ ] Tile partitioning, mode information, segmentation, delta quantization, transform-size selection, coefficient token decode, inverse quantization, and inverse transforms.
- [x] Match libaom's depth-first traversal and frame-edge behavior for all ten AV1 partition types. Independent 8/10/12-bit streams collectively select every terminal partition shape and contain nested block geometry that requires recursive `Split` traversal; their complete native planes remain byte-exact under normal hardware dispatch and the scalar fallback.
- [x] Verify the complete 8/10/12-bit monochrome, 4:2:0, 4:2:2, and 4:4:4 base profile matrix against pinned libaom native planes and pinned libavif presentation output. All 12 fixtures match exactly with normal dispatch, AVX-512 disabled, AVX disabled, and all hardware intrinsics disabled through `FeatureTestRunner`.
@ -524,8 +530,11 @@ Implement and verify in dependency order:
- [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.
- [x] 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 AVIF fixtures from the pinned generic libaom/libavif reference. The fixtures require actual intra-block-copy block selection, compare every native plane sample from retained scalar-decoder Y4M output, and compare every presented RGBA byte exactly under normal hardware dispatch and each narrower fallback configuration without a tolerance.
- [ ] Inter-frame prediction for layered still-image items.
- [x] Retain completed ungrained reference planes, frame headers, frame information, and published CDF snapshots in one eight-slot owner; apply refresh flags only after successful completion; resolve full and short reference signaling against occupancy and frame-ID validity; and implement primary-reference selection and `frame_size_with_refs`.
- [x] Inherit primary-reference CDFs, segmentation features and unchanged maps, loop-filter deltas, and same-role global-motion parameters. Initialize the frame-owned per-8x8 temporal motion field and project eligible retained motion vectors in normative reference order.
- [x] Implement allocation-free SIMD-first translational single-reference interpolation for regular, smooth, sharp, and bilinear filters; reduced four-sample kernels; horizontal, vertical, and separable two-dimensional convolution; exact AV1 rounding; 8/10/12-bit clipping; padded reference origins; and guarded destination strides. The operator contract and concrete operator files mirror JPEG color conversion, and `FeatureTestRunner` verifies normal, AVX-512-disabled, AVX-disabled, and scalar execution against an independent fixed-point oracle.
- [ ] Decode and connect reference indices, motion vectors, compound prediction, inter-intra prediction, masked blending, warped motion, global motion, and OBMC through reconstructed reference planes.
- [ ] Decode temporal segmentation prediction and consume the retained segmentation map when `segmentation_update_map == 1` and temporal update is enabled.
- [ ] Decode and connect inter-block reference indices, motion vectors, compound prediction, inter-intra prediction, masked blending, warped motion, global motion, and OBMC through reconstructed reference planes. The current tile mode reader accepts intra frames only.
- [ ] Verify every connected inter mode and filter with independently encoded dependent-layer AV1 image-item fixtures and exact native-plane comparisons.
- [ ] 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.

87
src/ImageSharp/Formats/Heif/Av1/Av1BitStreamReader.cs

@ -74,7 +74,7 @@ internal ref struct Av1BitStreamReader
/// Reads the next encoded bit as a Boolean value.
/// </summary>
/// <returns><see langword="true"/> for one; otherwise, <see langword="false"/>.</returns>
internal bool ReadBoolean() => this.ReadLiteral(1) > 0;
public bool ReadBoolean() => this.ReadLiteral(1) > 0;
/// <summary>
/// Reads an AV1 little-endian base-128 value from a byte-aligned position.
@ -159,6 +159,21 @@ internal ref struct Av1BitStreamReader
return (v << 1) - m + this.ReadLiteral(1);
}
/// <summary>
/// Reads a finite subexponential value recentered around a signed reference value.
/// </summary>
/// <param name="valueMagnitude">One greater than the maximum absolute value in the signed domain.</param>
/// <param name="groupBitCount">The bit width of the first subexponential group.</param>
/// <param name="reference">The signed reference value around which smaller codewords are concentrated.</param>
/// <returns>A decoded value in the inclusive range from minus <paramref name="valueMagnitude"/> plus one through
/// <paramref name="valueMagnitude"/> minus one.</returns>
public int ReadSignedReferenceSubexponential(int valueMagnitude, int groupBitCount, int reference)
{
int shiftedReference = reference + valueMagnitude - 1;
int scaledValueCount = (valueMagnitude << 1) - 1;
return this.ReadReferenceSubexponential(scaledValueCount, groupBitCount, shiftedReference) - valueMagnitude + 1;
}
/// <summary>
/// Reads a fixed-width two's-complement signed integer.
/// </summary>
@ -226,4 +241,74 @@ internal ref struct Av1BitStreamReader
this.Skip(byteCount << 3);
return payload;
}
/// <summary>
/// Reads a finite subexponential value and inverse-recenters it around an unsigned reference value.
/// </summary>
/// <param name="valueCount">The number of values in the finite domain.</param>
/// <param name="groupBitCount">The bit width of the first subexponential group.</param>
/// <param name="reference">The reference value within the finite domain.</param>
/// <returns>The decoded value in the range zero through <paramref name="valueCount"/> minus one.</returns>
private int ReadReferenceSubexponential(int valueCount, int groupBitCount, int reference)
{
int value = this.ReadSubexponential(valueCount, groupBitCount);
// Recentering enumerates values by increasing distance from the reference. References in the upper half use
// the mirrored domain so the shorter side of the finite range always participates in the alternating mapping.
if ((reference << 1) <= valueCount)
{
return InverseRecenter(reference, value);
}
return valueCount - 1 - InverseRecenter(valueCount - 1 - reference, value);
}
/// <summary>
/// Reads one value from a finite subexponential code.
/// </summary>
/// <param name="valueCount">The number of values in the finite domain.</param>
/// <param name="groupBitCount">The bit width of the first subexponential group.</param>
/// <returns>The decoded zero-based value.</returns>
private int ReadSubexponential(int valueCount, int groupBitCount)
{
int groupIndex = 0;
int groupStart = 0;
while (true)
{
// AV1 keeps the first two groups at width k and then doubles each following group. Once fewer than three
// groups remain, the non-symmetric code consumes the exact finite tail without introducing unused values.
int bitCount = groupIndex == 0 ? groupBitCount : groupBitCount + groupIndex - 1;
int groupSize = 1 << bitCount;
if (valueCount <= groupStart + (3 * groupSize))
{
return (int)this.ReadNonSymmetric((uint)(valueCount - groupStart)) + groupStart;
}
if (!this.ReadBoolean())
{
return (int)this.ReadLiteral(bitCount) + groupStart;
}
groupIndex++;
groupStart += groupSize;
}
}
/// <summary>
/// Maps a nonnegative code value around a nonnegative reference value.
/// </summary>
/// <param name="reference">The recentering reference.</param>
/// <param name="value">The coded nonnegative value.</param>
/// <returns>The inverse-recentered value.</returns>
private static int InverseRecenter(int reference, int value)
{
// Codes within twice the reference alternate above and below it: even values select the upper side and odd
// values select the lower side. Larger codes lie beyond the lower-side range and map directly to the tail.
if (value > (reference << 1))
{
return value;
}
return (value & 1) == 0 ? (value >> 1) + reference : reference - ((value + 1) >> 1);
}
}

342
src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs

@ -2,8 +2,11 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Color;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.FilmGrain;
using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
@ -13,7 +16,7 @@ using SixLabors.ImageSharp.PixelFormats.Utils;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Decodes one AV1 still-image elementary stream into an ImageSharp image.
/// Decodes one bounded AV1 image payload into an ImageSharp image.
/// </summary>
internal sealed class Av1Decoder : IAv1TileReader, IDisposable
{
@ -27,6 +30,37 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// The reconstructed references and selected presentation output owned by the current bounded decode session.
/// </summary>
private readonly Av1ReferenceFrameStore referenceFrames = new();
/// <summary>
/// The frame-base, tile-working, and published entropy contexts created for the first coded frame and then reused
/// for this bounded decoder session.
/// </summary>
private Av1FrameEntropyContexts? entropyContexts;
/// <summary>
/// The coded sequence governing the active reference map and reusable entropy session.
/// </summary>
private ObuSequenceHeader? entropySequenceHeader;
/// <summary>
/// The item codec configuration validated before reconstructing a completed frame.
/// </summary>
private Av1CodecConfiguration? codecConfiguration;
/// <summary>
/// The container color description applied before reconstructing a completed frame.
/// </summary>
private CicpProfile? containerColorProfile;
/// <summary>
/// The sequence header already validated for the current bounded payload.
/// </summary>
private ObuSequenceHeader? validatedSequenceHeader;
/// <summary>
/// The tile parser shared by all tile groups in the current frame.
/// </summary>
@ -49,26 +83,26 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
public Av1Decoder(Configuration configuration, byte operatingPointIndex)
{
this.configuration = configuration;
this.obuReader = new(operatingPointIndex);
this.obuReader = new(operatingPointIndex, this.referenceFrames);
}
/// <summary>
/// Gets the decoded frame header, or <see langword="null"/> before the stream provides one.
/// Gets the final retained shown-frame header, or <see langword="null"/> before a shown frame completes.
/// </summary>
public ObuFrameHeader? FrameHeader { get; private set; }
/// <summary>
/// Gets the decoded sequence header, or <see langword="null"/> before the stream provides one.
/// Gets the sequence header governing the final retained shown frame, or <see langword="null"/> before one completes.
/// </summary>
public ObuSequenceHeader? SequenceHeader { get; private set; }
/// <summary>
/// Gets the tile and superblock state for the decoded frame, or <see langword="null"/> before tile parsing completes.
/// Gets the tile and superblock state for the final retained shown frame, or <see langword="null"/> before one completes.
/// </summary>
public Av1FrameInfo? FrameInfo { get; private set; }
/// <summary>
/// Decodes an AV1 still-image elementary stream.
/// Decodes a bounded AV1 image payload and presents its final shown frame.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <param name="buffer">The complete AV1 elementary-stream payload.</param>
@ -196,7 +230,7 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
}
/// <summary>
/// Parses and reconstructs one AV1 frame while retaining its native component planes for the caller.
/// Parses every coded frame in an AV1 payload and returns the final shown frame's native component planes.
/// </summary>
/// <param name="buffer">The complete AV1 elementary-stream payload.</param>
/// <param name="containerColorProfile">
@ -211,69 +245,21 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
Av1CodecConfiguration? codecConfiguration,
out CicpProfile effectiveColorProfile)
{
this.codecConfiguration = codecConfiguration;
this.containerColorProfile = containerColorProfile;
this.validatedSequenceHeader = null;
this.SequenceHeader = null;
this.FrameHeader = null;
this.FrameInfo = null;
Av1BitStreamReader reader = new(buffer);
this.obuReader.ReadAll(ref reader, buffer.Length, () => this, false);
Guard.NotNull(this.tileReader, nameof(this.tileReader));
Guard.NotNull(this.SequenceHeader, nameof(this.SequenceHeader));
Guard.NotNull(this.FrameHeader, nameof(this.FrameHeader));
codecConfiguration?.Validate(this.SequenceHeader);
if (containerColorProfile is not null)
{
ObuColorConfig colorConfig = this.SequenceHeader.ColorConfig;
ObuColorPrimaries containerColorPrimaries = (ObuColorPrimaries)containerColorProfile.ColorPrimaries;
ObuTransferCharacteristics containerTransferCharacteristics =
(ObuTransferCharacteristics)containerColorProfile.TransferCharacteristics;
ObuMatrixCoefficients containerMatrixCoefficients =
(ObuMatrixCoefficients)containerColorProfile.MatrixCoefficients;
// AV1-ISOBMFF permits nclx to supply only bitstream fields explicitly coded as unspecified. A
// different specified value is a conformance error rather than a container-level color override.
if (colorConfig.ColorPrimaries == ObuColorPrimaries.Unspecified)
{
colorConfig.ColorPrimaries = containerColorPrimaries;
}
else if (colorConfig.ColorPrimaries != containerColorPrimaries)
{
throw new InvalidImageContentException("The HEIF CICP color primaries do not match the AV1 sequence header.");
}
if (colorConfig.TransferCharacteristics == ObuTransferCharacteristics.Unspecified)
{
colorConfig.TransferCharacteristics = containerTransferCharacteristics;
}
else if (colorConfig.TransferCharacteristics != containerTransferCharacteristics)
{
throw new InvalidImageContentException("The HEIF CICP transfer characteristics do not match the AV1 sequence header.");
}
if (colorConfig.MatrixCoefficients == ObuMatrixCoefficients.Unspecified)
{
colorConfig.MatrixCoefficients = containerMatrixCoefficients;
}
else if (colorConfig.MatrixCoefficients != containerMatrixCoefficients)
{
throw new InvalidImageContentException("The HEIF CICP matrix coefficients do not match the AV1 sequence header.");
}
if (colorConfig.ColorRange != containerColorProfile.FullRange)
{
throw new InvalidImageContentException("The HEIF CICP color range does not match the AV1 sequence header.");
}
}
this.FrameInfo = this.tileReader.FrameInfo;
Av1FrameBuffer<byte> frameBuffer = new(
this.configuration,
this.SequenceHeader,
this.SequenceHeader.ColorConfig.GetColorFormat(),
false);
try
{
using Av1FrameDecoder frameDecoder = new(this.SequenceHeader, this.FrameHeader, this.FrameInfo, frameBuffer);
frameDecoder.DecodeFrame();
this.obuReader.ReadAll(ref reader, buffer.Length, () => this, false);
Guard.NotNull(this.referenceFrames.OutputFrame, nameof(this.referenceFrames.OutputFrame));
Guard.NotNull(this.SequenceHeader, nameof(this.SequenceHeader));
Guard.NotNull(this.FrameHeader, nameof(this.FrameHeader));
// Preserve the effective CICP description used for conversion, including container values that legally
// supplied unspecified bitstream fields. This also exposes bitstream-only color metadata to callers.
@ -284,13 +270,31 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
(byte)effectiveColorConfig.MatrixCoefficients,
effectiveColorConfig.ColorRange);
return frameBuffer;
using Av1ReferenceFrame outputFrame = this.referenceFrames.TakeOutput();
return outputFrame.TakeFrameBuffer();
}
catch
{
frameBuffer.Dispose();
// A failed frame may own pooled neighbor contexts while earlier layers own reconstructed references and
// published CDF snapshots. None can be reused after a non-transactional frame transition has failed.
this.tileReader?.Dispose();
this.tileReader = null;
this.obuReader.Reset();
this.entropyContexts?.Reset();
this.entropySequenceHeader = null;
this.SequenceHeader = null;
this.FrameHeader = null;
this.FrameInfo = null;
throw;
}
finally
{
// Validation inputs belong to this bounded decode call. Completed native buffers retain no references to
// either description, so releasing them here prevents a reused decoder from observing stale item state.
this.codecConfiguration = null;
this.containerColorProfile = null;
this.validatedSequenceHeader = null;
}
}
/// <summary>
@ -302,24 +306,214 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
{
if (this.tileReader is null)
{
this.SequenceHeader = this.obuReader.SequenceHeader;
this.FrameHeader = this.obuReader.FrameHeader;
Guard.NotNull(this.SequenceHeader, nameof(this.SequenceHeader));
Guard.NotNull(this.FrameHeader, nameof(this.FrameHeader));
ObuSequenceHeader? sequenceHeader = this.obuReader.SequenceHeader;
ObuFrameHeader? frameHeader = this.obuReader.FrameHeader;
Guard.NotNull(sequenceHeader, nameof(sequenceHeader));
Guard.NotNull(frameHeader, nameof(frameHeader));
if (!ReferenceEquals(this.entropySequenceHeader, sequenceHeader))
{
if (this.entropySequenceHeader is not null)
{
// A coded-sequence boundary invalidates both sample references and their retained CDF snapshots.
// Returned snapshot graphs stay decoder-local and can be overwritten for the new sequence.
this.referenceFrames.Reset();
this.entropyContexts?.Reset();
}
this.entropySequenceHeader = sequenceHeader;
}
Av1FrameEntropyContext? primaryReferenceContext = null;
byte? primaryReferenceSlot = frameHeader.PrimaryReferenceSlot;
if (primaryReferenceSlot is not null)
{
// The uncompressed-header parser validates slot occupancy. Entropy ownership is checked here because
// only the reconstructed frame owner knows whether that slot retained a completed CDF snapshot.
Av1ReferenceFrame? primaryReference = this.referenceFrames.Resolve(primaryReferenceSlot.Value);
if (primaryReference is null || primaryReference.EntropyContext is null)
{
throw new InvalidImageContentException("The AV1 primary reference has no retained entropy context.");
}
primaryReferenceContext = primaryReference.EntropyContext;
}
// Every tile group in a frame contributes to the same mode-info and coefficient state.
this.tileReader = new Av1TileReader(this.configuration, this.SequenceHeader, this.FrameHeader);
Av1FrameEntropyContexts entropyContexts =
this.entropyContexts ??= new(frameHeader.QuantizationParameters.BaseQIndex);
this.tileReader = new Av1TileReader(
this.configuration,
sequenceHeader,
frameHeader,
entropyContexts,
primaryReferenceContext,
this.referenceFrames);
}
this.tileReader.ReadTile(tileData, tileNum);
}
/// <summary>
/// Releases the tile reader and its frame-scoped parsing storage.
/// Reconstructs a frame after all of its tile payloads have been parsed.
/// </summary>
public void CompleteFrame()
{
Av1TileReader tileReader = this.tileReader!;
ObuSequenceHeader sequenceHeader = this.obuReader.SequenceHeader!;
ObuFrameHeader frameHeader = this.obuReader.FrameHeader!;
Av1FrameBuffer<byte>? frameBuffer = null;
Av1FrameBuffer<byte>? presentationBuffer = null;
try
{
if (!ReferenceEquals(this.validatedSequenceHeader, sequenceHeader))
{
this.codecConfiguration?.Validate(sequenceHeader);
CicpProfile? colorProfile = this.containerColorProfile;
if (colorProfile is not null)
{
ObuColorConfig colorConfig = sequenceHeader.ColorConfig;
ObuColorPrimaries containerColorPrimaries = (ObuColorPrimaries)colorProfile.ColorPrimaries;
ObuTransferCharacteristics containerTransferCharacteristics =
(ObuTransferCharacteristics)colorProfile.TransferCharacteristics;
ObuMatrixCoefficients containerMatrixCoefficients =
(ObuMatrixCoefficients)colorProfile.MatrixCoefficients;
// AV1-ISOBMFF permits nclx to supply only bitstream fields explicitly coded as unspecified. A
// different specified value is a conformance error rather than a container-level color override.
if (colorConfig.ColorPrimaries == ObuColorPrimaries.Unspecified)
{
colorConfig.ColorPrimaries = containerColorPrimaries;
}
else if (colorConfig.ColorPrimaries != containerColorPrimaries)
{
throw new InvalidImageContentException("The HEIF CICP color primaries do not match the AV1 sequence header.");
}
if (colorConfig.TransferCharacteristics == ObuTransferCharacteristics.Unspecified)
{
colorConfig.TransferCharacteristics = containerTransferCharacteristics;
}
else if (colorConfig.TransferCharacteristics != containerTransferCharacteristics)
{
throw new InvalidImageContentException("The HEIF CICP transfer characteristics do not match the AV1 sequence header.");
}
if (colorConfig.MatrixCoefficients == ObuMatrixCoefficients.Unspecified)
{
colorConfig.MatrixCoefficients = containerMatrixCoefficients;
}
else if (colorConfig.MatrixCoefficients != containerMatrixCoefficients)
{
throw new InvalidImageContentException("The HEIF CICP matrix coefficients do not match the AV1 sequence header.");
}
if (colorConfig.ColorRange != colorProfile.FullRange)
{
throw new InvalidImageContentException("The HEIF CICP color range does not match the AV1 sequence header.");
}
}
// The same sequence header governs subsequent layered frames until another header OBU replaces it.
// Retaining the validated reference prevents repeated item/color checks in the common layered case.
this.validatedSequenceHeader = sequenceHeader;
}
Av1FrameInfo frameInfo = tileReader.FrameInfo;
frameBuffer = new Av1FrameBuffer<byte>(
this.configuration,
sequenceHeader,
sequenceHeader.ColorConfig.GetColorFormat(),
false);
using Av1FrameDecoder frameDecoder = new(sequenceHeader, frameHeader, frameInfo, frameBuffer);
frameDecoder.DecodeFrame();
bool retainsReference = (frameHeader.RefreshFrameFlags & byte.MaxValue) != 0;
if (retainsReference)
{
// Motion compensation may address any clamped position inside the decoder border. Extending once after
// all in-loop filters lets every later block use the full padded span without per-prediction edge copies.
Av1ReferenceFrameBorder.Extend(frameBuffer);
}
bool needsSeparatePresentation = frameHeader.ShowFrame && frameHeader.FilmGrainParameters.ApplyGrain && retainsReference;
if (needsSeparatePresentation)
{
presentationBuffer = new Av1FrameBuffer<byte>(
this.configuration,
sequenceHeader,
sequenceHeader.ColorConfig.GetColorFormat(),
false);
// Film grain must never contaminate a decoded reference. A shown frame that is also refreshed therefore
// receives one allocator-owned presentation copy; frames with no reference role are grained in place.
frameBuffer.CopyTo(presentationBuffer);
}
Av1FrameBuffer<byte> grainTarget = presentationBuffer ?? frameBuffer;
if (frameHeader.ShowFrame && frameHeader.FilmGrainParameters.ApplyGrain)
{
Av1FilmGrainDecoder filmGrainDecoder = new(sequenceHeader, frameHeader, grainTarget);
filmGrainDecoder.DecodeFrame();
}
Av1ReferenceFrame referenceFrame;
if (retainsReference)
{
Av1FrameEntropyContexts entropyContexts = this.entropyContexts!;
Av1FrameEntropyContext entropySnapshot = entropyContexts.RentPublishedSnapshot();
referenceFrame = new(frameBuffer, frameHeader, frameInfo, entropySnapshot, entropyContexts);
}
else
{
// Presentation-only frames can never become primary references, so they own no unused CDF graph.
referenceFrame = new(frameBuffer, frameHeader, frameInfo);
}
frameBuffer = null;
if (!this.referenceFrames.Commit(frameHeader.RefreshFrameFlags, referenceFrame, frameHeader.ShowFrame && !needsSeparatePresentation))
{
referenceFrame.Dispose();
}
if (presentationBuffer is not null)
{
Av1ReferenceFrame presentationFrame = new(presentationBuffer, frameHeader, frameInfo);
presentationBuffer = null;
this.referenceFrames.CommitOutput(presentationFrame);
}
if (frameHeader.ShowFrame)
{
this.SequenceHeader = sequenceHeader;
this.FrameHeader = frameHeader;
this.FrameInfo = frameInfo;
}
}
finally
{
// A non-shown frame or failed reconstruction never escapes this callback. FrameInfo uses managed storage,
// so it remains inspectable for a retained frame after the pooled entropy-neighbor contexts are returned.
presentationBuffer?.Dispose();
frameBuffer?.Dispose();
tileReader.Dispose();
this.tileReader = null;
}
}
/// <summary>
/// Releases the current tile parser, reference map, and retained presentation output.
/// </summary>
public void Dispose()
{
this.tileReader?.Dispose();
this.tileReader = null;
this.referenceFrames.Dispose();
}
}

58
src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs

@ -108,19 +108,29 @@ internal class Av1FrameBuffer<T> : IDisposable
this.BufferY = null;
this.BufferCb = null;
this.BufferCr = null;
if ((bufferEnableMask & PictureBufferYFlag) != 0)
try
{
this.BufferY = configuration.MemoryAllocator.Allocate2D<T>(strideY * this.storageElementsPerSample, heightY);
if ((bufferEnableMask & PictureBufferYFlag) != 0)
{
this.BufferY = configuration.MemoryAllocator.Allocate2D<T>(strideY * this.storageElementsPerSample, heightY);
}
if ((bufferEnableMask & PictureBufferCbFlag) != 0)
{
this.BufferCb = configuration.MemoryAllocator.Allocate2D<T>(strideChroma * this.storageElementsPerSample, heightChroma);
}
if ((bufferEnableMask & PictureBufferCrFlag) != 0)
{
this.BufferCr = configuration.MemoryAllocator.Allocate2D<T>(strideChroma * this.storageElementsPerSample, heightChroma);
}
}
if ((bufferEnableMask & PictureBufferCbFlag) != 0)
{
this.BufferCb = configuration.MemoryAllocator.Allocate2D<T>(strideChroma * this.storageElementsPerSample, heightChroma);
}
if ((bufferEnableMask & PictureBufferCrFlag) != 0)
catch
{
this.BufferCr = configuration.MemoryAllocator.Allocate2D<T>(strideChroma * this.storageElementsPerSample, heightChroma);
// Construction publishes the owner only after every required plane has been rented. Release earlier planes
// here because a later allocation failure leaves no constructed frame buffer for the caller to dispose.
this.Dispose();
throw;
}
}
@ -204,6 +214,34 @@ internal class Av1FrameBuffer<T> : IDisposable
/// </summary>
public MemoryAllocator MemoryAllocator { get; }
/// <summary>
/// Copies the complete padded sample planes and active picture geometry to another compatible frame buffer.
/// </summary>
/// <param name="destination">The frame buffer receiving the copied reconstruction.</param>
public void CopyTo(Av1FrameBuffer<T> destination)
{
// Copy each contiguous allocation so the runtime can use its optimized bulk-memory path. Film-grain
// presentation consumes right and bottom padding for odd dimensions, so copying only visible rows would leave
// part of the independently owned presentation surface undefined.
this.BufferY!.DangerousGetSingleSpan().CopyTo(destination.BufferY!.DangerousGetSingleSpan());
Buffer2D<T>? chromaBlue = this.BufferCb;
if (chromaBlue is not null)
{
chromaBlue.DangerousGetSingleSpan().CopyTo(destination.BufferCb!.DangerousGetSingleSpan());
this.BufferCr!.DangerousGetSingleSpan().CopyTo(destination.BufferCr!.DangerousGetSingleSpan());
}
destination.StartPosition = this.StartPosition;
destination.OriginX = this.OriginX;
destination.OriginY = this.OriginY;
destination.Width = this.Width;
destination.Height = this.Height;
destination.MaxWidth = this.MaxWidth;
destination.MaxHeight = this.MaxHeight;
destination.BitDepth = this.BitDepth;
destination.ColorFormat = this.ColorFormat;
}
/// <summary>
/// Releases the owned luma and chroma plane allocations.
/// </summary>

85
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs

@ -326,9 +326,10 @@ internal static class Av1DefaultDistributions
];
/// <summary>
/// Gets the end-of-block token distributions for transforms containing at most 16 coefficients, indexed by quantizer, plane, and transform-class contexts.
/// Gets the end-of-block token distributions for transforms containing at most 16 coefficients, indexed by
/// quantizer, plane, and transform-class contexts.
/// </summary>
private static Av1Distribution[][][] EndOfBlockFlagMulti16 =>
private static Av1Distribution[][][] EndOfBlockFlagMulti16 { get; } =
[
[
[new(840, 1039, 1980, 4895), new(370, 671, 1883, 4471)],
@ -349,9 +350,10 @@ internal static class Av1DefaultDistributions
];
/// <summary>
/// Gets the end-of-block token distributions for transforms containing at most 32 coefficients, indexed by quantizer, plane, and transform-class contexts.
/// Gets the end-of-block token distributions for transforms containing at most 32 coefficients, indexed by
/// quantizer, plane, and transform-class contexts.
/// </summary>
private static Av1Distribution[][][] EndOfBlockFlagMulti32 =>
private static Av1Distribution[][][] EndOfBlockFlagMulti32 { get; } =
[
[
[new(400, 520, 977, 2102, 6542), new(210, 405, 1315, 3326, 7537)],
@ -372,9 +374,10 @@ internal static class Av1DefaultDistributions
];
/// <summary>
/// Gets the end-of-block token distributions for transforms containing at most 64 coefficients, indexed by quantizer, plane, and transform-class contexts.
/// Gets the end-of-block token distributions for transforms containing at most 64 coefficients, indexed by
/// quantizer, plane, and transform-class contexts.
/// </summary>
private static Av1Distribution[][][] EndOfBlockFlagMulti64 =>
private static Av1Distribution[][][] EndOfBlockFlagMulti64 { get; } =
[
[
[new(329, 498, 1101, 1784, 3265, 7758), new(335, 730, 1459, 5494, 8755, 12997)],
@ -395,9 +398,10 @@ internal static class Av1DefaultDistributions
];
/// <summary>
/// Gets the end-of-block token distributions for transforms containing at most 128 coefficients, indexed by quantizer, plane, and transform-class contexts.
/// Gets the end-of-block token distributions for transforms containing at most 128 coefficients, indexed by
/// quantizer, plane, and transform-class contexts.
/// </summary>
private static Av1Distribution[][][] EndOfBlockFlagMulti128 =>
private static Av1Distribution[][][] EndOfBlockFlagMulti128 { get; } =
[
[
[new(219, 482, 1140, 2091, 3680, 6028, 12586), new(371, 699, 1254, 4830, 9479, 12562, 17497)],
@ -418,9 +422,10 @@ internal static class Av1DefaultDistributions
];
/// <summary>
/// Gets the end-of-block token distributions for transforms containing at most 256 coefficients, indexed by quantizer, plane, and transform-class contexts.
/// Gets the end-of-block token distributions for transforms containing at most 256 coefficients, indexed by
/// quantizer, plane, and transform-class contexts.
/// </summary>
private static Av1Distribution[][][] EndOfBlockFlagMulti256 =>
private static Av1Distribution[][][] EndOfBlockFlagMulti256 { get; } =
[
[
[
@ -465,9 +470,10 @@ internal static class Av1DefaultDistributions
];
/// <summary>
/// Gets the end-of-block token distributions for transforms containing at most 512 coefficients, indexed by quantizer, plane, and transform-class contexts.
/// Gets the end-of-block token distributions for transforms containing at most 512 coefficients, indexed by
/// quantizer, plane, and transform-class contexts.
/// </summary>
private static Av1Distribution[][][] EndOfBlockFlagMulti512 =>
private static Av1Distribution[][][] EndOfBlockFlagMulti512 { get; } =
[
[
[
@ -512,9 +518,10 @@ internal static class Av1DefaultDistributions
];
/// <summary>
/// Gets the end-of-block token distributions for transforms containing at most 1024 coded coefficients, indexed by quantizer, plane, and transform-class contexts.
/// Gets the end-of-block token distributions for transforms containing at most 1024 coded coefficients, indexed by
/// quantizer, plane, and transform-class contexts.
/// </summary>
private static Av1Distribution[][][] EndOfBlockFlagMulti1024 =>
private static Av1Distribution[][][] EndOfBlockFlagMulti1024 { get; } =
[
[
[
@ -559,9 +566,10 @@ internal static class Av1DefaultDistributions
];
/// <summary>
/// Gets the coefficient base-range distributions indexed by quantizer, transform-size, plane, and base-range contexts.
/// Gets the coefficient base-range distributions indexed by quantizer, transform-size, plane, and base-range
/// contexts.
/// </summary>
private static Av1Distribution[][][][] CoefficientsBaseRange =>
private static Av1Distribution[][][][] CoefficientsBaseRange { get; } =
[
[
[
@ -974,9 +982,10 @@ internal static class Av1DefaultDistributions
];
/// <summary>
/// Gets the coefficient base-level distributions indexed by quantizer, transform-size, plane, and nonzero-map contexts.
/// Gets the coefficient base-level distributions indexed by quantizer, transform-size, plane, and nonzero-map
/// contexts.
/// </summary>
private static Av1Distribution[][][][] CoefficientsBase =>
private static Av1Distribution[][][][] CoefficientsBase { get; } =
[
[
[
@ -1669,9 +1678,10 @@ internal static class Av1DefaultDistributions
];
/// <summary>
/// Gets the final-nonzero coefficient base-level distributions indexed by quantizer, transform-size, plane, and end-of-block contexts.
/// Gets the final-nonzero coefficient base-level distributions indexed by quantizer, transform-size, plane, and
/// end-of-block contexts.
/// </summary>
private static Av1Distribution[][][][] BaseEndOfBlock =>
private static Av1Distribution[][][][] BaseEndOfBlock { get; } =
[
[
[
@ -1766,7 +1776,7 @@ internal static class Av1DefaultDistributions
/// <summary>
/// Gets the DC coefficient-sign distributions indexed by quantizer, plane, and neighboring sign contexts.
/// </summary>
private static Av1Distribution[][][] DcSign =>
private static Av1Distribution[][][] DcSign { get; } =
[
[
[new(128 * 125), new(128 * 102), new(128 * 147)],
@ -1789,7 +1799,7 @@ internal static class Av1DefaultDistributions
/// <summary>
/// Gets the transform-block skip distributions indexed by quantizer, transform-size, and neighboring skip contexts.
/// </summary>
private static Av1Distribution[][][] TransformBlockSkip =>
private static Av1Distribution[][][] TransformBlockSkip { get; } =
[
[
[
@ -1882,9 +1892,10 @@ internal static class Av1DefaultDistributions
];
/// <summary>
/// Gets the end-of-block extra-bit distributions indexed by quantizer, transform-size, plane, and padded token contexts.
/// Gets the end-of-block extra-bit distributions indexed by quantizer, transform-size, plane, and padded token
/// contexts.
/// </summary>
private static Av1Distribution[][][][] EndOfBlockExtra =>
private static Av1Distribution[][][][] EndOfBlockExtra { get; } =
[
[
[
@ -2419,13 +2430,13 @@ internal static class Av1DefaultDistributions
int qContext = GetQContext(baseQIndex);
return
[
EndOfBlockFlagMulti16[qContext],
EndOfBlockFlagMulti32[qContext],
EndOfBlockFlagMulti64[qContext],
EndOfBlockFlagMulti128[qContext],
EndOfBlockFlagMulti256[qContext],
EndOfBlockFlagMulti512[qContext],
EndOfBlockFlagMulti1024[qContext],
Av1Distribution.CreateCopy(EndOfBlockFlagMulti16[qContext]),
Av1Distribution.CreateCopy(EndOfBlockFlagMulti32[qContext]),
Av1Distribution.CreateCopy(EndOfBlockFlagMulti64[qContext]),
Av1Distribution.CreateCopy(EndOfBlockFlagMulti128[qContext]),
Av1Distribution.CreateCopy(EndOfBlockFlagMulti256[qContext]),
Av1Distribution.CreateCopy(EndOfBlockFlagMulti512[qContext]),
Av1Distribution.CreateCopy(EndOfBlockFlagMulti1024[qContext]),
];
}
@ -2435,7 +2446,7 @@ internal static class Av1DefaultDistributions
/// <param name="baseQIndex">The frame base quantizer index.</param>
/// <returns>The distributions indexed by transform-size, plane, and base-range contexts.</returns>
public static Av1Distribution[][][] GetCoefficientsBaseRange(int baseQIndex)
=> CoefficientsBaseRange[GetQContext(baseQIndex)];
=> Av1Distribution.CreateCopy(CoefficientsBaseRange[GetQContext(baseQIndex)]);
/// <summary>
/// Gets the coefficient base-level distributions for a base quantizer.
@ -2443,7 +2454,7 @@ internal static class Av1DefaultDistributions
/// <param name="baseQIndex">The frame base quantizer index.</param>
/// <returns>The distributions indexed by transform-size, plane, and nonzero-map contexts.</returns>
public static Av1Distribution[][][] GetCoefficientsBase(int baseQIndex)
=> CoefficientsBase[GetQContext(baseQIndex)];
=> Av1Distribution.CreateCopy(CoefficientsBase[GetQContext(baseQIndex)]);
/// <summary>
/// Gets the final-nonzero coefficient base-level distributions for a base quantizer.
@ -2451,7 +2462,7 @@ internal static class Av1DefaultDistributions
/// <param name="baseQIndex">The frame base quantizer index.</param>
/// <returns>The distributions indexed by transform-size, plane, and end-of-block contexts.</returns>
public static Av1Distribution[][][] GetBaseEndOfBlock(int baseQIndex)
=> BaseEndOfBlock[GetQContext(baseQIndex)];
=> Av1Distribution.CreateCopy(BaseEndOfBlock[GetQContext(baseQIndex)]);
/// <summary>
/// Gets the DC sign distributions for a base quantizer.
@ -2459,7 +2470,7 @@ internal static class Av1DefaultDistributions
/// <param name="baseQIndex">The frame base quantizer index.</param>
/// <returns>The distributions indexed by plane and neighboring sign context.</returns>
public static Av1Distribution[][] GetDcSign(int baseQIndex)
=> DcSign[GetQContext(baseQIndex)];
=> Av1Distribution.CreateCopy(DcSign[GetQContext(baseQIndex)]);
/// <summary>
/// Gets the transform-block skip distributions for a base quantizer.
@ -2467,7 +2478,7 @@ internal static class Av1DefaultDistributions
/// <param name="baseQIndex">The frame base quantizer index.</param>
/// <returns>The distributions indexed by transform-size and neighboring skip context.</returns>
public static Av1Distribution[][] GetTransformBlockSkip(int baseQIndex)
=> TransformBlockSkip[GetQContext(baseQIndex)];
=> Av1Distribution.CreateCopy(TransformBlockSkip[GetQContext(baseQIndex)]);
/// <summary>
/// Gets the end-of-block extra-bit distributions for a base quantizer.
@ -2475,7 +2486,7 @@ internal static class Av1DefaultDistributions
/// <param name="baseQIndex">The frame base quantizer index.</param>
/// <returns>The distributions indexed by transform-size, plane, and end-of-block token context.</returns>
public static Av1Distribution[][][] GetEndOfBlockExtra(int baseQIndex)
=> EndOfBlockExtra[GetQContext(baseQIndex)];
=> Av1Distribution.CreateCopy(EndOfBlockExtra[GetQContext(baseQIndex)]);
/// <summary>
/// Maps a base quantizer index to one of the four AV1 coefficient-probability initialization bands.

52
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DisplacementVectorContext.cs

@ -35,6 +35,27 @@ internal sealed class Av1DisplacementVectorContext
/// </summary>
private readonly Component horizontal = new();
/// <summary>
/// Replaces every displacement-vector distribution with state copied from another context.
/// </summary>
/// <param name="source">The displacement-vector context state to copy.</param>
public void CopyFrom(Av1DisplacementVectorContext source)
{
this.joint.CopyFrom(source.joint);
this.vertical.CopyFrom(source.vertical);
this.horizontal.CopyFrom(source.horizontal);
}
/// <summary>
/// Resets every observation count used to adapt displacement-vector distributions.
/// </summary>
public void ResetUpdateCounts()
{
this.joint.ResetUpdateCount();
this.vertical.ResetUpdateCounts();
this.horizontal.ResetUpdateCounts();
}
/// <summary>
/// Reads an integer displacement vector relative to a spatially derived reference.
/// </summary>
@ -105,6 +126,37 @@ internal sealed class Av1DisplacementVectorContext
new(24576), new(28672), new(29952), new(29952), new(30720)
];
/// <summary>
/// Replaces every component distribution with state copied from another component.
/// </summary>
/// <param name="source">The component state to copy.</param>
public void CopyFrom(Component source)
{
this.magnitudeClass.CopyFrom(source.magnitudeClass);
this.sign.CopyFrom(source.sign);
this.classZero.CopyFrom(source.classZero);
for (int bit = 0; bit < this.offsetBits.Length; bit++)
{
this.offsetBits[bit].CopyFrom(source.offsetBits[bit]);
}
}
/// <summary>
/// Resets every observation count used to adapt one component's distributions.
/// </summary>
public void ResetUpdateCounts()
{
this.magnitudeClass.ResetUpdateCount();
this.sign.ResetUpdateCount();
this.classZero.ResetUpdateCount();
for (int bit = 0; bit < this.offsetBits.Length; bit++)
{
this.offsetBits[bit].ResetUpdateCount();
}
}
/// <summary>
/// Reads one signed integer-precision component.
/// </summary>

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

@ -11,22 +11,22 @@ internal class Av1Distribution
/// <summary>
/// The exclusive upper bound of the Q15 probability domain.
/// </summary>
internal const int ProbabilityTop = 1 << ProbabilityBitCount;
public const int ProbabilityTop = 1 << ProbabilityBitCount;
/// <summary>
/// The minimum sub-range reserved for each symbol during range coding.
/// </summary>
internal const int ProbabilityMinimum = 4;
public const int ProbabilityMinimum = 4;
/// <summary>
/// The shift that converts stored Q15 cumulative values to the range-coder precision.
/// </summary>
internal const int CdfShift = 15 - ProbabilityBitCount;
public const int CdfShift = 15 - ProbabilityBitCount;
/// <summary>
/// The precision reduction applied before multiplying a cumulative value by the coding range.
/// </summary>
internal const int ProbabilityShift = 6;
public const int ProbabilityShift = 6;
/// <summary>
/// The number of fractional bits in a stored cumulative probability.
@ -320,6 +320,23 @@ internal class Av1Distribution
/// <returns>A distribution initialized with the same probabilities and update count.</returns>
public Av1Distribution CreateCopy() => new(this);
/// <summary>
/// Replaces the probability and adaptation state with the state of another distribution having the same alphabet.
/// </summary>
/// <param name="source">The distribution state to copy.</param>
public void CopyFrom(Av1Distribution source)
{
// Entropy contexts are created from the same fixed default table shape. Copy only mutable state so resetting a
// working tile never allocates or replaces the distribution objects referenced by the symbol decoder.
source.probabilities.AsSpan().CopyTo(this.probabilities);
this.updateCount = source.updateCount;
}
/// <summary>
/// Resets the observation count that controls the adaptive update rate without changing probability thresholds.
/// </summary>
public void ResetUpdateCount() => this.updateCount = 0;
/// <summary>
/// Creates independently adaptable copies of a distribution array.
/// </summary>

519
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContext.cs

@ -0,0 +1,519 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Owns the adaptive AV1 distributions currently implemented by the frame and tile syntax decoders.
/// </summary>
/// <remarks>
/// One frame context supplies the initial state copied into every tile context. Each tile adapts an independent working
/// copy, and only the tile selected by <c>context_update_tile_id</c> supplies the completed frame snapshot.
/// </remarks>
internal sealed class Av1FrameEntropyContext
{
/// <summary>
/// The inclusive upper bound of the first AV1 coefficient-probability quantizer band.
/// </summary>
private const int FirstQuantizerBandMaximum = 20;
/// <summary>
/// The inclusive upper bound of the second AV1 coefficient-probability quantizer band.
/// </summary>
private const int SecondQuantizerBandMaximum = 60;
/// <summary>
/// The inclusive upper bound of the third AV1 coefficient-probability quantizer band.
/// </summary>
private const int ThirdQuantizerBandMaximum = 120;
/// <summary>
/// The immutable normative contexts used to restore reusable frame state without rebuilding distribution graphs.
/// </summary>
private static readonly Av1FrameEntropyContext[] DefaultPrototypes =
[
new((byte)0),
new((byte)1),
new((byte)2),
new((byte)3)
];
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameEntropyContext"/> class from the normative default
/// distributions selected by a frame quantizer index.
/// </summary>
/// <param name="qIndex">The frame base quantizer index selecting coefficient distribution defaults.</param>
public Av1FrameEntropyContext(int qIndex)
: this(DefaultPrototypes[GetQContext(qIndex)])
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameEntropyContext"/> class as an immutable normative prototype.
/// </summary>
/// <param name="qContext">The zero-based coefficient-probability quantizer band.</param>
private Av1FrameEntropyContext(byte qContext)
{
int qIndex = qContext switch
{
0 => 0,
1 => FirstQuantizerBandMaximum + 1,
2 => SecondQuantizerBandMaximum + 1,
_ => ThirdQuantizerBandMaximum + 1
};
// Every default-distribution accessor constructs independently mutable state. Retaining those returned
// graphs directly confines generated-table construction to the four process-wide quantizer-band prototypes.
this.IntraBlockCopy = Av1DefaultDistributions.IntraBlockCopy;
this.DisplacementVector = new();
this.SwitchableRestoration = Av1DefaultDistributions.SwitchableRestoration;
this.WienerRestoration = Av1DefaultDistributions.WienerRestoration;
this.SgrProjectionRestoration = Av1DefaultDistributions.SgrProjectionRestoration;
this.PaletteYMode = Av1DefaultDistributions.PaletteYMode;
this.PaletteUvMode = Av1DefaultDistributions.PaletteUvMode;
this.PaletteYSize = Av1DefaultDistributions.PaletteYSize;
this.PaletteUvSize = Av1DefaultDistributions.PaletteUvSize;
this.PaletteYColorIndex = Av1DefaultDistributions.PaletteYColorIndex;
this.PaletteUvColorIndex = Av1DefaultDistributions.PaletteUvColorIndex;
this.PartitionTypes = Av1DefaultDistributions.PartitionTypes;
this.KeyFrameYMode = Av1DefaultDistributions.KeyFrameYMode;
this.UvMode = Av1DefaultDistributions.UvMode;
this.Skip = Av1DefaultDistributions.Skip;
this.SkipMode = Av1DefaultDistributions.SkipMode;
this.DeltaLoopFilterAbsolute = Av1DefaultDistributions.DeltaLoopFilterAbsolute;
this.DeltaQuantizerAbsolute = Av1DefaultDistributions.DeltaQuantizerAbsolute;
this.SegmentId = Av1DefaultDistributions.SegmentId;
this.AngleDelta = Av1DefaultDistributions.AngleDelta;
this.FilterIntraMode = Av1DefaultDistributions.FilterIntraMode;
this.FilterIntra = Av1DefaultDistributions.FilterIntra;
this.TransformSize = Av1DefaultDistributions.TransformSize;
this.ChromaFromLumaSign = Av1DefaultDistributions.ChromaFromLumaSign;
this.ChromaFromLumaAlpha = Av1DefaultDistributions.ChromaFromLumaAlpha;
this.IntraExtendedTransform = Av1DefaultDistributions.IntraExtendedTransform;
this.InterExtendedTransform = Av1DefaultDistributions.InterExtendedTransform;
// Coefficient defaults use one of four quantizer bands. Their array shapes remain fixed, so later tile resets
// copy only thresholds and update counts into this context's already allocated distribution graph.
this.EndOfBlockFlag = Av1DefaultDistributions.GetEndOfBlockFlag(qIndex);
this.CoefficientsBase = Av1DefaultDistributions.GetCoefficientsBase(qIndex);
this.BaseEndOfBlock = Av1DefaultDistributions.GetBaseEndOfBlock(qIndex);
this.DcSign = Av1DefaultDistributions.GetDcSign(qIndex);
this.CoefficientsBaseRange = Av1DefaultDistributions.GetCoefficientsBaseRange(qIndex);
this.TransformBlockSkip = Av1DefaultDistributions.GetTransformBlockSkip(qIndex);
this.EndOfBlockExtra = Av1DefaultDistributions.GetEndOfBlockExtra(qIndex);
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameEntropyContext"/> class with an independently adaptable copy of a prototype.
/// </summary>
/// <param name="source">The prototype or retained context whose state is copied.</param>
private Av1FrameEntropyContext(Av1FrameEntropyContext source)
{
// Session and retained-frame contexts need one mutable graph, not four generated quantizer-band graphs whose
// unused bands are immediately discarded. Deep-copy the already selected prototype shape exactly once.
this.IntraBlockCopy = source.IntraBlockCopy.CreateCopy();
this.DisplacementVector = new();
this.DisplacementVector.CopyFrom(source.DisplacementVector);
this.SwitchableRestoration = source.SwitchableRestoration.CreateCopy();
this.WienerRestoration = source.WienerRestoration.CreateCopy();
this.SgrProjectionRestoration = source.SgrProjectionRestoration.CreateCopy();
this.PaletteYMode = Av1Distribution.CreateCopy(source.PaletteYMode);
this.PaletteUvMode = Av1Distribution.CreateCopy(source.PaletteUvMode);
this.PaletteYSize = Av1Distribution.CreateCopy(source.PaletteYSize);
this.PaletteUvSize = Av1Distribution.CreateCopy(source.PaletteUvSize);
this.PaletteYColorIndex = Av1Distribution.CreateCopy(source.PaletteYColorIndex);
this.PaletteUvColorIndex = Av1Distribution.CreateCopy(source.PaletteUvColorIndex);
this.PartitionTypes = Av1Distribution.CreateCopy(source.PartitionTypes);
this.KeyFrameYMode = Av1Distribution.CreateCopy(source.KeyFrameYMode);
this.UvMode = Av1Distribution.CreateCopy(source.UvMode);
this.Skip = Av1Distribution.CreateCopy(source.Skip);
this.SkipMode = Av1Distribution.CreateCopy(source.SkipMode);
this.DeltaLoopFilterAbsolute = source.DeltaLoopFilterAbsolute.CreateCopy();
this.DeltaQuantizerAbsolute = source.DeltaQuantizerAbsolute.CreateCopy();
this.SegmentId = Av1Distribution.CreateCopy(source.SegmentId);
this.AngleDelta = Av1Distribution.CreateCopy(source.AngleDelta);
this.FilterIntraMode = source.FilterIntraMode.CreateCopy();
this.FilterIntra = Av1Distribution.CreateCopy(source.FilterIntra);
this.TransformSize = Av1Distribution.CreateCopy(source.TransformSize);
this.EndOfBlockFlag = Av1Distribution.CreateCopy(source.EndOfBlockFlag);
this.CoefficientsBase = Av1Distribution.CreateCopy(source.CoefficientsBase);
this.BaseEndOfBlock = Av1Distribution.CreateCopy(source.BaseEndOfBlock);
this.DcSign = Av1Distribution.CreateCopy(source.DcSign);
this.CoefficientsBaseRange = Av1Distribution.CreateCopy(source.CoefficientsBaseRange);
this.TransformBlockSkip = Av1Distribution.CreateCopy(source.TransformBlockSkip);
this.EndOfBlockExtra = Av1Distribution.CreateCopy(source.EndOfBlockExtra);
this.ChromaFromLumaSign = source.ChromaFromLumaSign.CreateCopy();
this.ChromaFromLumaAlpha = Av1Distribution.CreateCopy(source.ChromaFromLumaAlpha);
this.IntraExtendedTransform = Av1Distribution.CreateCopy(source.IntraExtendedTransform);
this.InterExtendedTransform = Av1Distribution.CreateCopy(source.InterExtendedTransform);
}
/// <summary>
/// Gets the intra-block-copy distribution.
/// </summary>
public Av1Distribution IntraBlockCopy { get; }
/// <summary>
/// Gets the integer displacement-vector context used by intra-block copy.
/// </summary>
public Av1DisplacementVectorContext DisplacementVector { get; }
/// <summary>
/// Gets the switchable loop-restoration distribution.
/// </summary>
public Av1Distribution SwitchableRestoration { get; }
/// <summary>
/// Gets the Wiener loop-restoration distribution.
/// </summary>
public Av1Distribution WienerRestoration { get; }
/// <summary>
/// Gets the self-guided loop-restoration distribution.
/// </summary>
public Av1Distribution SgrProjectionRestoration { get; }
/// <summary>
/// Gets the luma palette-mode distributions.
/// </summary>
public Av1Distribution[][] PaletteYMode { get; }
/// <summary>
/// Gets the chroma palette-mode distributions.
/// </summary>
public Av1Distribution[] PaletteUvMode { get; }
/// <summary>
/// Gets the luma palette-size distributions.
/// </summary>
public Av1Distribution[] PaletteYSize { get; }
/// <summary>
/// Gets the chroma palette-size distributions.
/// </summary>
public Av1Distribution[] PaletteUvSize { get; }
/// <summary>
/// Gets the luma palette color-index distributions.
/// </summary>
public Av1Distribution[][] PaletteYColorIndex { get; }
/// <summary>
/// Gets the chroma palette color-index distributions.
/// </summary>
public Av1Distribution[][] PaletteUvColorIndex { get; }
/// <summary>
/// Gets the partition-type distributions.
/// </summary>
public Av1Distribution[] PartitionTypes { get; }
/// <summary>
/// Gets the key-frame luma-mode distributions.
/// </summary>
public Av1Distribution[][] KeyFrameYMode { get; }
/// <summary>
/// Gets the chroma intra-mode distributions.
/// </summary>
public Av1Distribution[][] UvMode { get; }
/// <summary>
/// Gets the transform-skip distributions.
/// </summary>
public Av1Distribution[] Skip { get; }
/// <summary>
/// Gets the skip-mode distributions.
/// </summary>
public Av1Distribution[] SkipMode { get; }
/// <summary>
/// Gets the absolute loop-filter delta distribution.
/// </summary>
public Av1Distribution DeltaLoopFilterAbsolute { get; }
/// <summary>
/// Gets the absolute quantizer delta distribution.
/// </summary>
public Av1Distribution DeltaQuantizerAbsolute { get; }
/// <summary>
/// Gets the spatial segment-identifier distributions.
/// </summary>
public Av1Distribution[] SegmentId { get; }
/// <summary>
/// Gets the directional angle-delta distributions.
/// </summary>
public Av1Distribution[] AngleDelta { get; }
/// <summary>
/// Gets the filter-intra mode distribution.
/// </summary>
public Av1Distribution FilterIntraMode { get; }
/// <summary>
/// Gets the filter-intra enable distributions.
/// </summary>
public Av1Distribution[] FilterIntra { get; }
/// <summary>
/// Gets the transform-size distributions.
/// </summary>
public Av1Distribution[][] TransformSize { get; }
/// <summary>
/// Gets the end-of-block token distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][][] EndOfBlockFlag { get; }
/// <summary>
/// Gets the coefficient base-level distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][][] CoefficientsBase { get; }
/// <summary>
/// Gets the final-nonzero coefficient distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][][] BaseEndOfBlock { get; }
/// <summary>
/// Gets the DC sign distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][] DcSign { get; }
/// <summary>
/// Gets the coefficient base-range distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][][] CoefficientsBaseRange { get; }
/// <summary>
/// Gets the transform-block skip distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][] TransformBlockSkip { get; }
/// <summary>
/// Gets the end-of-block extra-bit distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][][] EndOfBlockExtra { get; }
/// <summary>
/// Gets the joint chroma-from-luma sign distribution.
/// </summary>
public Av1Distribution ChromaFromLumaSign { get; }
/// <summary>
/// Gets the chroma-from-luma alpha-magnitude distributions.
/// </summary>
public Av1Distribution[] ChromaFromLumaAlpha { get; }
/// <summary>
/// Gets the intra transform-type distributions.
/// </summary>
public Av1Distribution[][][] IntraExtendedTransform { get; }
/// <summary>
/// Gets the inter transform-type distributions.
/// </summary>
public Av1Distribution[][] InterExtendedTransform { get; }
/// <summary>
/// Restores the normative frame defaults selected by a base quantizer index.
/// </summary>
/// <param name="qIndex">The frame base quantizer index selecting coefficient distribution defaults.</param>
public void ResetToDefaults(int qIndex)
{
int qContext = GetQContext(qIndex);
// The prototypes are never exposed to a range reader. Copying their state lets a decoder session reuse the
// same three mutable object graphs even when successive frames select different coefficient-model bands.
this.CopyFrom(DefaultPrototypes[qContext]);
}
/// <summary>
/// Maps a frame base quantizer to its normative coefficient-probability initialization band.
/// </summary>
/// <param name="qIndex">The frame base quantizer index.</param>
/// <returns>The zero-based quantizer-band index.</returns>
private static int GetQContext(int qIndex)
=> qIndex switch
{
<= FirstQuantizerBandMaximum => 0,
<= SecondQuantizerBandMaximum => 1,
<= ThirdQuantizerBandMaximum => 2,
_ => 3
};
/// <summary>
/// Replaces every probability threshold and adaptation count with state copied from another frame context.
/// </summary>
/// <param name="source">The frame context state to copy.</param>
public void CopyFrom(Av1FrameEntropyContext source)
{
this.IntraBlockCopy.CopyFrom(source.IntraBlockCopy);
this.DisplacementVector.CopyFrom(source.DisplacementVector);
this.SwitchableRestoration.CopyFrom(source.SwitchableRestoration);
this.WienerRestoration.CopyFrom(source.WienerRestoration);
this.SgrProjectionRestoration.CopyFrom(source.SgrProjectionRestoration);
CopyState(source.PaletteYMode, this.PaletteYMode);
CopyState(source.PaletteUvMode, this.PaletteUvMode);
CopyState(source.PaletteYSize, this.PaletteYSize);
CopyState(source.PaletteUvSize, this.PaletteUvSize);
CopyState(source.PaletteYColorIndex, this.PaletteYColorIndex);
CopyState(source.PaletteUvColorIndex, this.PaletteUvColorIndex);
CopyState(source.PartitionTypes, this.PartitionTypes);
CopyState(source.KeyFrameYMode, this.KeyFrameYMode);
CopyState(source.UvMode, this.UvMode);
CopyState(source.Skip, this.Skip);
CopyState(source.SkipMode, this.SkipMode);
this.DeltaLoopFilterAbsolute.CopyFrom(source.DeltaLoopFilterAbsolute);
this.DeltaQuantizerAbsolute.CopyFrom(source.DeltaQuantizerAbsolute);
CopyState(source.SegmentId, this.SegmentId);
CopyState(source.AngleDelta, this.AngleDelta);
this.FilterIntraMode.CopyFrom(source.FilterIntraMode);
CopyState(source.FilterIntra, this.FilterIntra);
CopyState(source.TransformSize, this.TransformSize);
CopyState(source.EndOfBlockFlag, this.EndOfBlockFlag);
CopyState(source.CoefficientsBase, this.CoefficientsBase);
CopyState(source.BaseEndOfBlock, this.BaseEndOfBlock);
CopyState(source.DcSign, this.DcSign);
CopyState(source.CoefficientsBaseRange, this.CoefficientsBaseRange);
CopyState(source.TransformBlockSkip, this.TransformBlockSkip);
CopyState(source.EndOfBlockExtra, this.EndOfBlockExtra);
this.ChromaFromLumaSign.CopyFrom(source.ChromaFromLumaSign);
CopyState(source.ChromaFromLumaAlpha, this.ChromaFromLumaAlpha);
CopyState(source.IntraExtendedTransform, this.IntraExtendedTransform);
CopyState(source.InterExtendedTransform, this.InterExtendedTransform);
}
/// <summary>
/// Copies this tile-adapted context into a destination used as completed frame state.
/// </summary>
/// <param name="destination">The independently owned frame context that receives the snapshot.</param>
/// <remarks>
/// AV1 resets CDF observation counters after publishing the context-update tile. The copied thresholds remain
/// adapted, while the next frame starts its update-rate history from zero.
/// </remarks>
public void SnapshotTo(Av1FrameEntropyContext destination)
{
destination.CopyFrom(this);
destination.ResetUpdateCounts();
}
/// <summary>
/// Resets the observation count of every distribution without changing its probability thresholds.
/// </summary>
private void ResetUpdateCounts()
{
this.IntraBlockCopy.ResetUpdateCount();
this.DisplacementVector.ResetUpdateCounts();
this.SwitchableRestoration.ResetUpdateCount();
this.WienerRestoration.ResetUpdateCount();
this.SgrProjectionRestoration.ResetUpdateCount();
ResetUpdateCounts(this.PaletteYMode);
ResetUpdateCounts(this.PaletteUvMode);
ResetUpdateCounts(this.PaletteYSize);
ResetUpdateCounts(this.PaletteUvSize);
ResetUpdateCounts(this.PaletteYColorIndex);
ResetUpdateCounts(this.PaletteUvColorIndex);
ResetUpdateCounts(this.PartitionTypes);
ResetUpdateCounts(this.KeyFrameYMode);
ResetUpdateCounts(this.UvMode);
ResetUpdateCounts(this.Skip);
ResetUpdateCounts(this.SkipMode);
this.DeltaLoopFilterAbsolute.ResetUpdateCount();
this.DeltaQuantizerAbsolute.ResetUpdateCount();
ResetUpdateCounts(this.SegmentId);
ResetUpdateCounts(this.AngleDelta);
this.FilterIntraMode.ResetUpdateCount();
ResetUpdateCounts(this.FilterIntra);
ResetUpdateCounts(this.TransformSize);
ResetUpdateCounts(this.EndOfBlockFlag);
ResetUpdateCounts(this.CoefficientsBase);
ResetUpdateCounts(this.BaseEndOfBlock);
ResetUpdateCounts(this.DcSign);
ResetUpdateCounts(this.CoefficientsBaseRange);
ResetUpdateCounts(this.TransformBlockSkip);
ResetUpdateCounts(this.EndOfBlockExtra);
this.ChromaFromLumaSign.ResetUpdateCount();
ResetUpdateCounts(this.ChromaFromLumaAlpha);
ResetUpdateCounts(this.IntraExtendedTransform);
ResetUpdateCounts(this.InterExtendedTransform);
}
/// <summary>
/// Copies one distribution row into an existing row with the same default-table shape.
/// </summary>
/// <param name="source">The source distribution row.</param>
/// <param name="destination">The destination distribution row.</param>
private static void CopyState(Av1Distribution[] source, Av1Distribution[] destination)
{
for (int index = 0; index < source.Length; index++)
{
destination[index].CopyFrom(source[index]);
}
}
/// <summary>
/// Copies a two-dimensional distribution table into an existing table with the same default-table shape.
/// </summary>
/// <param name="source">The source distribution table.</param>
/// <param name="destination">The destination distribution table.</param>
private static void CopyState(Av1Distribution[][] source, Av1Distribution[][] destination)
{
for (int index = 0; index < source.Length; index++)
{
CopyState(source[index], destination[index]);
}
}
/// <summary>
/// Copies a three-dimensional distribution table into an existing table with the same default-table shape.
/// </summary>
/// <param name="source">The source distribution table.</param>
/// <param name="destination">The destination distribution table.</param>
private static void CopyState(Av1Distribution[][][] source, Av1Distribution[][][] destination)
{
for (int index = 0; index < source.Length; index++)
{
CopyState(source[index], destination[index]);
}
}
/// <summary>
/// Resets observation counts in one distribution row.
/// </summary>
/// <param name="distributions">The distribution row to reset.</param>
private static void ResetUpdateCounts(Av1Distribution[] distributions)
{
for (int index = 0; index < distributions.Length; index++)
{
distributions[index].ResetUpdateCount();
}
}
/// <summary>
/// Resets observation counts in a two-dimensional distribution table.
/// </summary>
/// <param name="distributions">The distribution table to reset.</param>
private static void ResetUpdateCounts(Av1Distribution[][] distributions)
{
for (int index = 0; index < distributions.Length; index++)
{
ResetUpdateCounts(distributions[index]);
}
}
/// <summary>
/// Resets observation counts in a three-dimensional distribution table.
/// </summary>
/// <param name="distributions">The distribution table to reset.</param>
private static void ResetUpdateCounts(Av1Distribution[][][] distributions)
{
for (int index = 0; index < distributions.Length; index++)
{
ResetUpdateCounts(distributions[index]);
}
}
}

150
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContexts.cs

@ -0,0 +1,150 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Owns the reusable frame-base, tile-working, and published AV1 entropy contexts for one decoder session.
/// </summary>
internal sealed class Av1FrameEntropyContexts
{
/// <summary>
/// The maximum number of live reference-map and presentation owners plus the newly reconstructed frame awaiting
/// commit.
/// </summary>
private const int MaximumSnapshotCount = Av1Constants.ReferenceFrameCount + 2;
/// <summary>
/// Session-local returned snapshot graphs available for later refreshed frames.
/// </summary>
private InlineArray10<Av1FrameEntropyContext?> returnedSnapshots;
/// <summary>
/// The number of returned snapshot graphs currently available for reuse.
/// </summary>
private int returnedSnapshotCount;
/// <summary>
/// The base quantizer index used to initialize a newly required snapshot graph.
/// </summary>
private int currentQIndex;
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameEntropyContexts"/> class.
/// </summary>
/// <param name="qIndex">The initial frame base quantizer index.</param>
public Av1FrameEntropyContexts(int qIndex)
{
this.Base = new(qIndex);
this.Working = new(qIndex);
this.Published = new(qIndex);
this.currentQIndex = qIndex;
}
/// <summary>
/// Gets the unchanged frame context from which each independently decoded tile starts.
/// </summary>
public Av1FrameEntropyContext Base { get; }
/// <summary>
/// Gets the tile-local context reused sequentially for each tile in the current frame.
/// </summary>
public Av1FrameEntropyContext Working { get; }
/// <summary>
/// Gets the completed frame context selected by the signaled context-update tile, or the unchanged frame-base
/// context when frame-end updates are disabled.
/// </summary>
public Av1FrameEntropyContext Published { get; }
/// <summary>
/// Initializes frame entropy state from either a retained primary reference or normative quantizer-band defaults.
/// </summary>
/// <param name="qIndex">The frame base quantizer index selecting coefficient distribution defaults.</param>
/// <param name="primaryReferenceContext">
/// The retained primary-reference context, or <see langword="null"/> when the frame selects normative defaults.
/// </param>
public void BeginFrame(int qIndex, Av1FrameEntropyContext? primaryReferenceContext)
{
this.currentQIndex = qIndex;
if (primaryReferenceContext is null)
{
this.Base.ResetToDefaults(qIndex);
}
else
{
// A retained context is independent from the working and published graphs. Copying it here preserves the
// reference owner's snapshot while the current frame adapts its own tile-local state.
this.Base.CopyFrom(primaryReferenceContext);
}
// The context-update tile can precede later tiles. Published therefore cannot alias Working: a later tile
// must be free to overwrite Working while the selected completed-frame state remains available to the owner.
this.Base.SnapshotTo(this.Published);
}
/// <summary>
/// Clears active frame entropy state when a new coded sequence invalidates the complete reference map.
/// </summary>
public void Reset()
{
this.currentQIndex = 0;
this.Base.ResetToDefaults(this.currentQIndex);
this.Base.SnapshotTo(this.Working);
this.Base.SnapshotTo(this.Published);
// Returned graphs contain no live reference state and remain private to this decoder. Retaining them here
// allows the next sequence to reuse peak reference ownership without a static cross-decode pool.
}
/// <summary>
/// Rents an independently owned, reset-counter snapshot of the completed frame entropy context.
/// </summary>
/// <returns>The snapshot that must later be returned through <see cref="ReturnSnapshot"/>.</returns>
public Av1FrameEntropyContext RentPublishedSnapshot()
{
Av1FrameEntropyContext snapshot;
if (this.returnedSnapshotCount == 0)
{
// Eight slots can own distinct frames while the selected output owns a ninth frame no longer present in
// the map. Rent one further graph before commit releases the owner displaced by the completed frame.
snapshot = new(this.currentQIndex);
}
else
{
int snapshotIndex = --this.returnedSnapshotCount;
snapshot = this.returnedSnapshots[snapshotIndex]!;
this.returnedSnapshots[snapshotIndex] = null;
}
this.Published.SnapshotTo(snapshot);
return snapshot;
}
/// <summary>
/// Returns a retained-frame entropy snapshot to this decoder session for later reuse.
/// </summary>
/// <param name="snapshot">The snapshot whose reference-frame ownership has ended.</param>
public void ReturnSnapshot(Av1FrameEntropyContext snapshot)
{
// The fixed capacity covers eight distinct slot owners, one detached presentation owner, and the replacement
// frame rented before commit. Av1ReferenceFrame returns each graph exactly once, so the session cannot exceed
// this bound.
this.returnedSnapshots[this.returnedSnapshotCount++] = snapshot;
}
/// <summary>
/// Provides inline storage for every entropy snapshot graph that one decoder session can allocate concurrently.
/// </summary>
/// <typeparam name="T">The stored reference type.</typeparam>
[InlineArray(MaximumSnapshotCount)]
private struct InlineArray10<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
}

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

@ -439,11 +439,9 @@ internal static class Av1SymbolContextHelper
/// <returns>The transform type associated with the selected prediction mode.</returns>
internal static Av1TransformType ConvertIntraModeToTransformType(Av1BlockModeInfo modeInfo, Av1PlaneType planeType)
{
Av1PredictionMode mode = (planeType == Av1PlaneType.Y) ? modeInfo.YMode : modeInfo.UvMode;
if (mode == Av1PredictionMode.UvChromaFromLuma)
{
mode = Av1PredictionMode.DC;
}
// libaom's get_uv_mode() is the explicit boundary between the distinct UV and luma prediction domains. CfL maps
// to DC because the chroma AC contribution is applied to a DC predictor before coefficient reconstruction.
Av1PredictionMode mode = planeType == Av1PlaneType.Y ? modeInfo.YMode : modeInfo.UvMode.ToLumaMode();
return mode.ToTransformType();
}

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

@ -15,179 +15,9 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
internal ref struct Av1SymbolDecoder
{
/// <summary>
/// Maps each intra prediction mode to the reduced neighbor context used by key-frame luma modes.
/// The independently adaptable distribution graph for the current tile.
/// </summary>
private static readonly int[] IntraModeContext = [0, 1, 2, 3, 4, 4, 4, 4, 3, 0, 1, 2, 0];
/// <summary>
/// The tile-adaptive intra-block-copy distribution.
/// </summary>
private readonly Av1Distribution tileIntraBlockCopy;
/// <summary>
/// The tile-adaptive integer displacement-vector context used by intra-block copy.
/// </summary>
private readonly Av1DisplacementVectorContext displacementVector;
/// <summary>
/// The tile-adaptive switchable loop-restoration distribution.
/// </summary>
private readonly Av1Distribution switchableRestoration;
/// <summary>
/// The tile-adaptive Wiener loop-restoration distribution.
/// </summary>
private readonly Av1Distribution wienerRestoration;
/// <summary>
/// The tile-adaptive self-guided loop-restoration distribution.
/// </summary>
private readonly Av1Distribution sgrProjectionRestoration;
/// <summary>
/// The tile-adaptive luma palette-mode distributions.
/// </summary>
private readonly Av1Distribution[][] paletteYMode;
/// <summary>
/// The tile-adaptive chroma palette-mode distributions.
/// </summary>
private readonly Av1Distribution[] paletteUvMode;
/// <summary>
/// The tile-adaptive luma palette-size distributions.
/// </summary>
private readonly Av1Distribution[] paletteYSize;
/// <summary>
/// The tile-adaptive chroma palette-size distributions.
/// </summary>
private readonly Av1Distribution[] paletteUvSize;
/// <summary>
/// The tile-adaptive luma palette color-index distributions.
/// </summary>
private readonly Av1Distribution[][] paletteYColorIndex;
/// <summary>
/// The tile-adaptive chroma palette color-index distributions.
/// </summary>
private readonly Av1Distribution[][] paletteUvColorIndex;
/// <summary>
/// The tile-adaptive partition-type distributions.
/// </summary>
private readonly Av1Distribution[] tilePartitionTypes;
/// <summary>
/// The tile-adaptive key-frame luma-mode distributions.
/// </summary>
private readonly Av1Distribution[][] keyFrameYMode;
/// <summary>
/// The tile-adaptive chroma intra-mode distributions.
/// </summary>
private readonly Av1Distribution[][] uvMode;
/// <summary>
/// The tile-adaptive transform-skip distributions.
/// </summary>
private readonly Av1Distribution[] skip;
/// <summary>
/// The tile-adaptive skip-mode distributions.
/// </summary>
private readonly Av1Distribution[] skipMode;
/// <summary>
/// The tile-adaptive absolute loop-filter delta distribution.
/// </summary>
private readonly Av1Distribution deltaLoopFilterAbsolute;
/// <summary>
/// The tile-adaptive absolute quantizer delta distribution.
/// </summary>
private readonly Av1Distribution deltaQuantizerAbsolute;
/// <summary>
/// The tile-adaptive spatial segment-identifier distributions.
/// </summary>
private readonly Av1Distribution[] segmentId;
/// <summary>
/// The tile-adaptive directional angle-delta distributions.
/// </summary>
private readonly Av1Distribution[] angleDelta;
/// <summary>
/// The tile-adaptive filter-intra mode distribution.
/// </summary>
private readonly Av1Distribution filterIntraMode;
/// <summary>
/// The tile-adaptive filter-intra enable distributions.
/// </summary>
private readonly Av1Distribution[] filterIntra;
/// <summary>
/// The tile-adaptive transform-size distributions.
/// </summary>
private readonly Av1Distribution[][] transformSize;
/// <summary>
/// The tile-adaptive end-of-block token distributions selected for the frame base quantizer.
/// </summary>
private readonly Av1Distribution[][][] endOfBlockFlag;
/// <summary>
/// The tile-adaptive coefficient base-level distributions selected for the frame base quantizer.
/// </summary>
private readonly Av1Distribution[][][] coefficientsBase;
/// <summary>
/// The tile-adaptive final-nonzero coefficient distributions selected for the frame base quantizer.
/// </summary>
private readonly Av1Distribution[][][] baseEndOfBlock;
/// <summary>
/// The tile-adaptive DC sign distributions selected for the frame base quantizer.
/// </summary>
private readonly Av1Distribution[][] dcSign;
/// <summary>
/// The tile-adaptive coefficient base-range distributions selected for the frame base quantizer.
/// </summary>
private readonly Av1Distribution[][][] coefficientsBaseRange;
/// <summary>
/// The tile-adaptive transform-block skip distributions selected for the frame base quantizer.
/// </summary>
private readonly Av1Distribution[][] transformBlockSkip;
/// <summary>
/// The tile-adaptive end-of-block extra-bit distributions selected for the frame base quantizer.
/// </summary>
private readonly Av1Distribution[][][] endOfBlockExtra;
/// <summary>
/// The tile-adaptive joint chroma-from-luma sign distribution.
/// </summary>
private readonly Av1Distribution chromaFromLumaSign;
/// <summary>
/// The tile-adaptive chroma-from-luma alpha-magnitude distributions.
/// </summary>
private readonly Av1Distribution[] chromaFromLumaAlpha;
/// <summary>
/// The tile-adaptive intra transform-type distributions.
/// </summary>
private readonly Av1Distribution[][][] intraExtendedTransform;
/// <summary>
/// The tile-adaptive inter transform-type distributions.
/// </summary>
private readonly Av1Distribution[][] interExtendedTransform;
private readonly Av1FrameEntropyContext context;
/// <summary>
/// The configuration providing temporary coefficient-context memory.
@ -207,47 +37,42 @@ internal ref struct Av1SymbolDecoder
/// <param name="qIndex">The frame base quantizer index.</param>
/// <param name="updateCdf">A value indicating whether decoded symbols adapt their tile distributions.</param>
public Av1SymbolDecoder(Configuration configuration, Span<byte> tileData, int qIndex, bool updateCdf = true)
: this(configuration, tileData, new Av1FrameEntropyContext(qIndex), updateCdf)
{
// Every tile starts from its own frame-context copy. Sharing these objects would let one image's adaptive
// updates change the initial probabilities used to decode the next tile or image.
this.tileIntraBlockCopy = Av1DefaultDistributions.IntraBlockCopy.CreateCopy();
this.displacementVector = new();
this.switchableRestoration = Av1DefaultDistributions.SwitchableRestoration.CreateCopy();
this.wienerRestoration = Av1DefaultDistributions.WienerRestoration.CreateCopy();
this.sgrProjectionRestoration = Av1DefaultDistributions.SgrProjectionRestoration.CreateCopy();
this.paletteYMode = Av1Distribution.CreateCopy(Av1DefaultDistributions.PaletteYMode);
this.paletteUvMode = Av1Distribution.CreateCopy(Av1DefaultDistributions.PaletteUvMode);
this.paletteYSize = Av1Distribution.CreateCopy(Av1DefaultDistributions.PaletteYSize);
this.paletteUvSize = Av1Distribution.CreateCopy(Av1DefaultDistributions.PaletteUvSize);
this.paletteYColorIndex = Av1Distribution.CreateCopy(Av1DefaultDistributions.PaletteYColorIndex);
this.paletteUvColorIndex = Av1Distribution.CreateCopy(Av1DefaultDistributions.PaletteUvColorIndex);
this.tilePartitionTypes = Av1Distribution.CreateCopy(Av1DefaultDistributions.PartitionTypes);
this.keyFrameYMode = Av1Distribution.CreateCopy(Av1DefaultDistributions.KeyFrameYMode);
this.uvMode = Av1Distribution.CreateCopy(Av1DefaultDistributions.UvMode);
this.skip = Av1Distribution.CreateCopy(Av1DefaultDistributions.Skip);
this.skipMode = Av1Distribution.CreateCopy(Av1DefaultDistributions.SkipMode);
this.deltaLoopFilterAbsolute = Av1DefaultDistributions.DeltaLoopFilterAbsolute.CreateCopy();
this.deltaQuantizerAbsolute = Av1DefaultDistributions.DeltaQuantizerAbsolute.CreateCopy();
this.segmentId = Av1Distribution.CreateCopy(Av1DefaultDistributions.SegmentId);
this.angleDelta = Av1Distribution.CreateCopy(Av1DefaultDistributions.AngleDelta);
this.filterIntraMode = Av1DefaultDistributions.FilterIntraMode.CreateCopy();
this.filterIntra = Av1Distribution.CreateCopy(Av1DefaultDistributions.FilterIntra);
this.transformSize = Av1Distribution.CreateCopy(Av1DefaultDistributions.TransformSize);
this.chromaFromLumaSign = Av1DefaultDistributions.ChromaFromLumaSign.CreateCopy();
this.chromaFromLumaAlpha = Av1Distribution.CreateCopy(Av1DefaultDistributions.ChromaFromLumaAlpha);
this.intraExtendedTransform = Av1Distribution.CreateCopy(Av1DefaultDistributions.IntraExtendedTransform);
this.interExtendedTransform = Av1Distribution.CreateCopy(Av1DefaultDistributions.InterExtendedTransform);
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1SymbolDecoder"/> struct over a caller-owned tile entropy
/// context.
/// </summary>
/// <param name="configuration">The configuration providing temporary memory.</param>
/// <param name="tileData">The entropy-coded tile payload.</param>
/// <param name="context">The independently adaptable context initialized for this tile.</param>
/// <param name="updateCdf">A value indicating whether decoded symbols adapt their tile distributions.</param>
public Av1SymbolDecoder(
Configuration configuration,
Span<byte> tileData,
Av1FrameEntropyContext context,
bool updateCdf)
{
// The context owner controls reset and publication. Holding one reference here keeps the range decoder small
// and prevents a second set of aliases from becoming a competing source of entropy state.
this.context = context;
this.configuration = configuration;
this.reader = new Av1SymbolReader(tileData, updateCdf);
this.endOfBlockFlag = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetEndOfBlockFlag(qIndex));
this.coefficientsBase = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetCoefficientsBase(qIndex));
this.baseEndOfBlock = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetBaseEndOfBlock(qIndex));
this.dcSign = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetDcSign(qIndex));
this.coefficientsBaseRange = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetCoefficientsBaseRange(qIndex));
this.transformBlockSkip = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetTransformBlockSkip(qIndex));
this.endOfBlockExtra = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetEndOfBlockExtra(qIndex));
}
/// <summary>
/// Gets the reduced neighbor context for each intra prediction mode used by key-frame luma modes.
/// </summary>
private static ReadOnlySpan<int> IntraModeContext => [0, 1, 2, 3, 4, 4, 4, 4, 3, 0, 1, 2, 0];
/// <summary>
/// Validates that range decoding remained within the bounded tile payload and consumed the required trailing-one bit.
/// </summary>
public void ValidateTrailingBits()
=> this.reader.ValidateTrailingBits();
/// <summary>
/// Reads a fixed-width CDEF strength index.
/// </summary>
@ -315,7 +140,7 @@ internal ref struct Av1SymbolDecoder
public Av1RestorationFilterType ReadSwitchableRestorationType()
{
ref Av1SymbolReader r = ref this.reader;
return (Av1RestorationFilterType)r.ReadSymbol(this.switchableRestoration);
return (Av1RestorationFilterType)r.ReadSymbol(this.context.SwitchableRestoration);
}
/// <summary>
@ -325,7 +150,7 @@ internal ref struct Av1SymbolDecoder
public bool ReadWienerRestoration()
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.wienerRestoration) != 0;
return r.ReadSymbol(this.context.WienerRestoration) != 0;
}
/// <summary>
@ -335,7 +160,7 @@ internal ref struct Av1SymbolDecoder
public bool ReadSgrProjectionRestoration()
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.sgrProjectionRestoration) != 0;
return r.ReadSymbol(this.context.SgrProjectionRestoration) != 0;
}
/// <summary>
@ -398,7 +223,7 @@ internal ref struct Av1SymbolDecoder
public bool ReadPaletteYMode(int blockSizeContext, int neighborContext)
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.paletteYMode[blockSizeContext][neighborContext]) != 0;
return r.ReadSymbol(this.context.PaletteYMode[blockSizeContext][neighborContext]) != 0;
}
/// <summary>
@ -409,7 +234,7 @@ internal ref struct Av1SymbolDecoder
public bool ReadPaletteUvMode(bool hasLumaPalette)
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.paletteUvMode[hasLumaPalette ? 1 : 0]) != 0;
return r.ReadSymbol(this.context.PaletteUvMode[hasLumaPalette ? 1 : 0]) != 0;
}
/// <summary>
@ -422,8 +247,8 @@ internal ref struct Av1SymbolDecoder
{
ref Av1SymbolReader r = ref this.reader;
Av1Distribution distribution = planeType == Av1PlaneType.Y
? this.paletteYSize[blockSizeContext]
: this.paletteUvSize[blockSizeContext];
? this.context.PaletteYSize[blockSizeContext]
: this.context.PaletteUvSize[blockSizeContext];
return r.ReadSymbol(distribution) + 2;
}
@ -439,8 +264,8 @@ internal ref struct Av1SymbolDecoder
{
ref Av1SymbolReader r = ref this.reader;
Av1Distribution distribution = planeType == Av1PlaneType.Y
? this.paletteYColorIndex[paletteSize - 2][colorContext]
: this.paletteUvColorIndex[paletteSize - 2][colorContext];
? this.context.PaletteYColorIndex[paletteSize - 2][colorContext]
: this.context.PaletteUvColorIndex[paletteSize - 2][colorContext];
return r.ReadSymbol(distribution);
}
@ -452,7 +277,7 @@ internal ref struct Av1SymbolDecoder
public bool ReadUseIntraBlockCopy()
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.tileIntraBlockCopy) > 0;
return r.ReadSymbol(this.context.IntraBlockCopy) > 0;
}
/// <summary>
@ -461,7 +286,7 @@ internal ref struct Av1SymbolDecoder
/// <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);
=> this.context.DisplacementVector.Read(ref this.reader, reference);
/// <summary>
/// Reads a complete block partition type from the selected partition context.
@ -471,7 +296,7 @@ internal ref struct Av1SymbolDecoder
public Av1PartitionType ReadPartitionType(int context)
{
ref Av1SymbolReader r = ref this.reader;
return (Av1PartitionType)r.ReadSymbol(this.tilePartitionTypes[context]);
return (Av1PartitionType)r.ReadSymbol(this.context.PartitionTypes[context]);
}
/// <summary>
@ -482,7 +307,7 @@ internal ref struct Av1SymbolDecoder
/// <returns><see cref="Av1PartitionType.Split"/> or <see cref="Av1PartitionType.Horizontal"/>.</returns>
public Av1PartitionType ReadSplitOrHorizontal(Av1BlockSize blockSize, int context)
{
uint frequency = GetSplitOrHorizontalFrequency(this.tilePartitionTypes, blockSize, context);
uint frequency = GetSplitOrHorizontalFrequency(this.context.PartitionTypes, blockSize, context);
ref Av1SymbolReader r = ref this.reader;
return r.ReadBoolean(frequency) ? Av1PartitionType.Split : Av1PartitionType.Horizontal;
}
@ -495,7 +320,7 @@ internal ref struct Av1SymbolDecoder
/// <returns><see cref="Av1PartitionType.Split"/> or <see cref="Av1PartitionType.Vertical"/>.</returns>
public Av1PartitionType ReadSplitOrVertical(Av1BlockSize blockSize, int context)
{
uint frequency = GetSplitOrVerticalFrequency(this.tilePartitionTypes, blockSize, context);
uint frequency = GetSplitOrVerticalFrequency(this.context.PartitionTypes, blockSize, context);
ref Av1SymbolReader r = ref this.reader;
return r.ReadBoolean(frequency) ? Av1PartitionType.Split : Av1PartitionType.Vertical;
}
@ -523,7 +348,7 @@ internal ref struct Av1SymbolDecoder
int aboveContext = IntraModeContext[(int)aboveMode];
int leftContext = IntraModeContext[(int)leftMode];
return (Av1PredictionMode)r.ReadSymbol(this.keyFrameYMode[aboveContext][leftContext]);
return (Av1PredictionMode)r.ReadSymbol(this.context.KeyFrameYMode[aboveContext][leftContext]);
}
/// <summary>
@ -532,11 +357,11 @@ internal ref struct Av1SymbolDecoder
/// <param name="mode">The decoded luma prediction mode.</param>
/// <param name="chromaFromLumaAllowed">Indicates whether chroma-from-luma is valid for the block.</param>
/// <returns>The decoded chroma prediction mode.</returns>
public Av1PredictionMode ReadIntraModeUv(Av1PredictionMode mode, bool chromaFromLumaAllowed)
public Av1ChromaPredictionMode ReadIntraModeUv(Av1PredictionMode mode, bool chromaFromLumaAllowed)
{
int chromaForLumaIndex = chromaFromLumaAllowed ? 1 : 0;
ref Av1SymbolReader r = ref this.reader;
return (Av1PredictionMode)r.ReadSymbol(this.uvMode[chromaForLumaIndex][(int)mode]);
return (Av1ChromaPredictionMode)r.ReadSymbol(this.context.UvMode[chromaForLumaIndex][(int)mode]);
}
/// <summary>
@ -547,7 +372,7 @@ internal ref struct Av1SymbolDecoder
public bool ReadSkip(int ctx)
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.skip[ctx]) > 0;
return r.ReadSymbol(this.context.Skip[ctx]) > 0;
}
/// <summary>
@ -558,7 +383,7 @@ internal ref struct Av1SymbolDecoder
public bool ReadSkipMode(int context)
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.skipMode[context]) > 0;
return r.ReadSymbol(this.context.SkipMode[context]) > 0;
}
/// <summary>
@ -568,7 +393,7 @@ internal ref struct Av1SymbolDecoder
public int ReadDeltaLoopFilter()
{
ref Av1SymbolReader r = ref this.reader;
int deltaLoopFilterAbsolute = r.ReadSymbol(this.deltaLoopFilterAbsolute);
int deltaLoopFilterAbsolute = r.ReadSymbol(this.context.DeltaLoopFilterAbsolute);
if (deltaLoopFilterAbsolute == Av1Constants.DeltaLoopFilterSmall)
{
int deltaLoopFilterRemainingBits = r.ReadLiteral(3) + 1;
@ -592,7 +417,7 @@ internal ref struct Av1SymbolDecoder
public int ReadDeltaQuantizerIndex()
{
ref Av1SymbolReader r = ref this.reader;
int deltaQuantizerAbsolute = r.ReadSymbol(this.deltaQuantizerAbsolute);
int deltaQuantizerAbsolute = r.ReadSymbol(this.context.DeltaQuantizerAbsolute);
if (deltaQuantizerAbsolute == Av1Constants.DeltaQuantizerSmall)
{
int deltaQuantizerRemainingBits = r.ReadLiteral(3) + 1;
@ -617,7 +442,7 @@ internal ref struct Av1SymbolDecoder
public int ReadSegmentId(int context)
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.segmentId[context]);
return r.ReadSymbol(this.context.SegmentId[context]);
}
/// <summary>
@ -628,7 +453,7 @@ internal ref struct Av1SymbolDecoder
public int ReadAngleDelta(Av1PredictionMode mode)
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.angleDelta[(int)mode - 1]);
return r.ReadSymbol(this.context.AngleDelta[(int)mode - 1]);
}
/// <summary>
@ -640,10 +465,10 @@ internal ref struct Av1SymbolDecoder
{
ref Av1SymbolReader r = ref this.reader;
Av1FilterIntraMode filterIntraMode = Av1FilterIntraMode.AllFilterIntraModes;
bool useFilterIntra = r.ReadSymbol(this.filterIntra[(int)blockSize]) > 0;
bool useFilterIntra = r.ReadSymbol(this.context.FilterIntra[(int)blockSize]) > 0;
if (useFilterIntra)
{
filterIntraMode = (Av1FilterIntraMode)r.ReadSymbol(this.filterIntraMode);
filterIntraMode = (Av1FilterIntraMode)r.ReadSymbol(this.context.FilterIntraMode);
}
return filterIntraMode;
@ -669,7 +494,7 @@ internal ref struct Av1SymbolDecoder
DebugGuard.MustBeLessThanOrEqualTo(depth, Av1Constants.MaxTransformCategories, nameof(depth));
int category = depth - 1;
int value = r.ReadSymbol(this.transformSize[category][context]);
int value = r.ReadSymbol(this.context.TransformSize[category][context]);
Av1TransformSize transformSize = blockSize.GetMaximumTransformSize();
for (int d = 0; d < value; ++d)
{
@ -716,7 +541,7 @@ internal ref struct Av1SymbolDecoder
int symbol;
if (isInter)
{
symbol = r.ReadSymbol(this.interExtendedTransform[extendedSet][(int)squareTransformSize]);
symbol = r.ReadSymbol(this.context.InterExtendedTransform[extendedSet][(int)squareTransformSize]);
}
else
{
@ -724,7 +549,7 @@ internal ref struct Av1SymbolDecoder
? filterIntraMode.ToIntraDirection()
: intraDirection;
symbol = r.ReadSymbol(this.intraExtendedTransform[extendedSet][(int)squareTransformSize][(int)intraMode]);
symbol = r.ReadSymbol(this.context.IntraExtendedTransform[extendedSet][(int)squareTransformSize][(int)intraMode]);
}
transformType = Av1SymbolContextHelper.GetExtendedTransformType(transformSetType, symbol);
@ -742,7 +567,7 @@ internal ref struct Av1SymbolDecoder
public bool ReadTransformBlockSkip(Av1TransformSize transformSizeContext, int skipContext)
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.transformBlockSkip[(int)transformSizeContext][skipContext]) > 0;
return r.ReadSymbol(this.context.TransformBlockSkip[(int)transformSizeContext][skipContext]) > 0;
}
/// <summary>
@ -752,7 +577,7 @@ internal ref struct Av1SymbolDecoder
public int ReadChromFromLumaSign()
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.chromaFromLumaSign);
return r.ReadSymbol(this.context.ChromaFromLumaSign);
}
/// <summary>
@ -764,7 +589,7 @@ internal ref struct Av1SymbolDecoder
{
ref Av1SymbolReader r = ref this.reader;
int context = Av1ChromaFromLumaMath.ContextU(jointSignPlus1 - 1);
return r.ReadSymbol(this.chromaFromLumaAlpha[context]);
return r.ReadSymbol(this.context.ChromaFromLumaAlpha[context]);
}
/// <summary>
@ -776,7 +601,7 @@ internal ref struct Av1SymbolDecoder
{
ref Av1SymbolReader r = ref this.reader;
int context = Av1ChromaFromLumaMath.ContextV(jointSignPlus1 - 1);
return r.ReadSymbol(this.chromaFromLumaAlpha[context]);
return r.ReadSymbol(this.context.ChromaFromLumaAlpha[context]);
}
/// <summary>
@ -1082,7 +907,7 @@ internal ref struct Av1SymbolDecoder
int endOfBlockContext = transformClass == Av1TransformClass.Class2D ? 0 : 1;
int endOfBlockMultiSize = transformSize.GetLog2Minus4();
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.endOfBlockFlag[endOfBlockMultiSize][(int)planeType][endOfBlockContext]) + 1;
return r.ReadSymbol(this.context.EndOfBlockFlag[endOfBlockMultiSize][(int)planeType][endOfBlockContext]) + 1;
}
/// <summary>
@ -1095,7 +920,7 @@ internal ref struct Av1SymbolDecoder
private bool ReadEndOfBlockExtra(Av1TransformSize transformSizeContext, Av1PlaneType planeType, int endOfBlockContext)
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.endOfBlockExtra[(int)transformSizeContext][(int)planeType][endOfBlockContext]) > 0;
return r.ReadSymbol(this.context.EndOfBlockExtra[(int)transformSizeContext][(int)planeType][endOfBlockContext]) > 0;
}
/// <summary>
@ -1108,7 +933,7 @@ internal ref struct Av1SymbolDecoder
private int ReadCoefficientsBaseRange(Av1TransformSize transformSizeContext, Av1PlaneType planeType, int baseRangeContext)
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.coefficientsBaseRange[(int)transformSizeContext][(int)planeType][baseRangeContext]);
return r.ReadSymbol(this.context.CoefficientsBaseRange[(int)transformSizeContext][(int)planeType][baseRangeContext]);
}
/// <summary>
@ -1120,7 +945,7 @@ internal ref struct Av1SymbolDecoder
private int ReadDcSign(Av1PlaneType planeType, int dcSignContext)
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.dcSign[(int)planeType][dcSignContext]);
return r.ReadSymbol(this.context.DcSign[(int)planeType][dcSignContext]);
}
/// <summary>
@ -1133,7 +958,7 @@ internal ref struct Av1SymbolDecoder
private int ReadBaseEndOfBlock(Av1TransformSize transformSizeContext, Av1PlaneType planeType, int coefficientContext)
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.baseEndOfBlock[(int)transformSizeContext][(int)planeType][coefficientContext]);
return r.ReadSymbol(this.context.BaseEndOfBlock[(int)transformSizeContext][(int)planeType][coefficientContext]);
}
/// <summary>
@ -1146,7 +971,7 @@ internal ref struct Av1SymbolDecoder
private int ReadCoefficientsBase(Av1TransformSize transformSizeContext, Av1PlaneType planeType, int coefficientContext)
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.coefficientsBase[(int)transformSizeContext][(int)planeType][coefficientContext]);
return r.ReadSymbol(this.context.CoefficientsBase[(int)transformSizeContext][(int)planeType][coefficientContext]);
}
/// <summary>
@ -1160,7 +985,7 @@ internal ref struct Av1SymbolDecoder
{
ref Av1SymbolReader r = ref this.reader;
Av1TransformSize limitedTransformSizeContext = (Av1TransformSize)Math.Min((int)transformSizeContext, (int)Av1TransformSize.Size32x32);
Av1Distribution distribution = this.coefficientsBaseRange[(int)limitedTransformSizeContext][(int)planeType][baseRangeContext];
Av1Distribution distribution = this.context.CoefficientsBaseRange[(int)limitedTransformSizeContext][(int)planeType][baseRangeContext];
for (int idx = 0; idx < Av1Constants.CoefficientBaseRange; idx += Av1Constants.BaseRangeSizeMinus1)
{
int coefficientBaseRange = r.ReadSymbol(distribution);
@ -1177,7 +1002,7 @@ internal ref struct Av1SymbolDecoder
/// </summary>
/// <returns>The decoded nonnegative suffix value.</returns>
/// <exception cref="InvalidImageContentException">The unary prefix exceeds the AV1 coefficient limit.</exception>
internal int ReadGolomb()
public int ReadGolomb()
{
ref Av1SymbolReader r = ref this.reader;
int x = 1;
@ -1320,7 +1145,7 @@ internal ref struct Av1SymbolDecoder
/// <param name="blockSize">The current block size.</param>
/// <param name="context">The partition probability context.</param>
/// <returns>The Q15 probability of the split outcome.</returns>
internal static uint GetSplitOrHorizontalFrequency(Av1Distribution[] inputs, Av1BlockSize blockSize, int context)
public static uint GetSplitOrHorizontalFrequency(Av1Distribution[] inputs, Av1BlockSize blockSize, int context)
{
Av1Distribution input = inputs[context];
@ -1346,7 +1171,7 @@ internal ref struct Av1SymbolDecoder
/// <param name="blockSize">The current block size.</param>
/// <param name="context">The partition probability context.</param>
/// <returns>The Q15 probability of the split outcome.</returns>
internal static uint GetSplitOrVerticalFrequency(Av1Distribution[] inputs, Av1BlockSize blockSize, int context)
public static uint GetSplitOrVerticalFrequency(Av1Distribution[] inputs, Av1BlockSize blockSize, int context)
{
Av1Distribution input = inputs[context];

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

@ -155,29 +155,29 @@ internal class Av1SymbolEncoder : IDisposable
/// <param name="updateCdf">A value indicating whether encoded symbols adapt their tile distributions.</param>
public Av1SymbolEncoder(Configuration configuration, int initialSize, int qIndex, bool updateCdf = true)
{
// Encoding and decoding must begin from equivalent tile-local models. Copying the defaults also prevents
// one encoded image from changing the probabilities used by later encoder or decoder instances.
this.tileIntraBlockCopy = Av1DefaultDistributions.IntraBlockCopy.CreateCopy();
this.tilePartitionTypes = Av1Distribution.CreateCopy(Av1DefaultDistributions.PartitionTypes);
this.keyFrameYMode = Av1Distribution.CreateCopy(Av1DefaultDistributions.KeyFrameYMode);
this.uvMode = Av1Distribution.CreateCopy(Av1DefaultDistributions.UvMode);
this.filterIntra = Av1Distribution.CreateCopy(Av1DefaultDistributions.FilterIntra);
this.filterIntraMode = Av1DefaultDistributions.FilterIntraMode.CreateCopy();
this.deltaQuantizerAbsolute = Av1DefaultDistributions.DeltaQuantizerAbsolute.CreateCopy();
this.intraExtendedTransform = Av1Distribution.CreateCopy(Av1DefaultDistributions.IntraExtendedTransform);
this.segmentId = Av1Distribution.CreateCopy(Av1DefaultDistributions.SegmentId);
this.angleDelta = Av1Distribution.CreateCopy(Av1DefaultDistributions.AngleDelta);
this.skip = Av1Distribution.CreateCopy(Av1DefaultDistributions.Skip);
this.skipMode = Av1Distribution.CreateCopy(Av1DefaultDistributions.SkipMode);
this.chromaFromLumaSign = Av1DefaultDistributions.ChromaFromLumaSign.CreateCopy();
this.chromaFromLumaAlpha = Av1Distribution.CreateCopy(Av1DefaultDistributions.ChromaFromLumaAlpha);
this.transformBlockSkip = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetTransformBlockSkip(qIndex));
this.endOfBlockFlag = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetEndOfBlockFlag(qIndex));
this.coefficientsBaseRange = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetCoefficientsBaseRange(qIndex));
this.coefficientsBase = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetCoefficientsBase(qIndex));
this.coefficientsBaseEndOfBlock = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetBaseEndOfBlock(qIndex));
this.dcSign = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetDcSign(qIndex));
this.endOfBlockExtra = Av1Distribution.CreateCopy(Av1DefaultDistributions.GetEndOfBlockExtra(qIndex));
// Every default accessor creates independently mutable state. Encoding and decoding therefore begin from
// equivalent tile-local models without constructing and immediately deep-copying a second object graph.
this.tileIntraBlockCopy = Av1DefaultDistributions.IntraBlockCopy;
this.tilePartitionTypes = Av1DefaultDistributions.PartitionTypes;
this.keyFrameYMode = Av1DefaultDistributions.KeyFrameYMode;
this.uvMode = Av1DefaultDistributions.UvMode;
this.filterIntra = Av1DefaultDistributions.FilterIntra;
this.filterIntraMode = Av1DefaultDistributions.FilterIntraMode;
this.deltaQuantizerAbsolute = Av1DefaultDistributions.DeltaQuantizerAbsolute;
this.intraExtendedTransform = Av1DefaultDistributions.IntraExtendedTransform;
this.segmentId = Av1DefaultDistributions.SegmentId;
this.angleDelta = Av1DefaultDistributions.AngleDelta;
this.skip = Av1DefaultDistributions.Skip;
this.skipMode = Av1DefaultDistributions.SkipMode;
this.chromaFromLumaSign = Av1DefaultDistributions.ChromaFromLumaSign;
this.chromaFromLumaAlpha = Av1DefaultDistributions.ChromaFromLumaAlpha;
this.transformBlockSkip = Av1DefaultDistributions.GetTransformBlockSkip(qIndex);
this.endOfBlockFlag = Av1DefaultDistributions.GetEndOfBlockFlag(qIndex);
this.coefficientsBaseRange = Av1DefaultDistributions.GetCoefficientsBaseRange(qIndex);
this.coefficientsBase = Av1DefaultDistributions.GetCoefficientsBase(qIndex);
this.coefficientsBaseEndOfBlock = Av1DefaultDistributions.GetBaseEndOfBlock(qIndex);
this.dcSign = Av1DefaultDistributions.GetDcSign(qIndex);
this.endOfBlockExtra = Av1DefaultDistributions.GetEndOfBlockExtra(qIndex);
this.configuration = configuration;
this.writer = new(configuration, initialSize, updateCdf);
this.baseQIndex = qIndex;
@ -375,7 +375,7 @@ internal class Av1SymbolEncoder : IDisposable
/// <param name="transformClass">The transform direction class.</param>
/// <param name="transformSize">The signaled transform size selecting the token alphabet.</param>
/// <param name="transformSizeContext">The square transform-size probability context.</param>
internal void WriteEndOfBlockPosition(ushort endOfBlock, Av1ComponentType componentType, Av1TransformClass transformClass, Av1TransformSize transformSize, Av1TransformSize transformSizeContext)
public void WriteEndOfBlockPosition(ushort endOfBlock, Av1ComponentType componentType, Av1TransformClass transformClass, Av1TransformSize transformSize, Av1TransformSize transformSizeContext)
{
short endOfBlockPosition = Av1SymbolContextHelper.GetEndOfBlockPosition(endOfBlock, out int eobExtra);
this.WriteEndOfBlockFlag(componentType, transformClass, transformSize, endOfBlockPosition);
@ -406,7 +406,7 @@ internal class Av1SymbolEncoder : IDisposable
/// <param name="skip">Indicates whether the transform block is empty.</param>
/// <param name="transformSizeContext">The square transform-size probability context.</param>
/// <param name="skipContext">The context derived from neighboring coefficient blocks.</param>
internal void WriteTransformBlockSkip(bool skip, Av1TransformSize transformSizeContext, int skipContext)
public void WriteTransformBlockSkip(bool skip, Av1TransformSize transformSizeContext, int skipContext)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol(skip, this.transformBlockSkip[(int)transformSizeContext][skipContext]);
@ -438,7 +438,7 @@ internal class Av1SymbolEncoder : IDisposable
/// Writes the unsigned exponential-Golomb suffix used for coefficient levels beyond the base range.
/// </summary>
/// <param name="level">The nonnegative suffix value.</param>
internal void WriteGolomb(int level)
public void WriteGolomb(int level)
{
uint x = (uint)level + 1u;
int length = (int)Av1Math.Log2_32(x) + 1;
@ -481,7 +481,7 @@ internal class Av1SymbolEncoder : IDisposable
/// <param name="baseQIndex">The active base quantizer index.</param>
/// <param name="filterIntraMode">The filter-intra mode when enabled.</param>
/// <param name="intraDirection">The ordinary intra prediction mode.</param>
internal void WriteTransformType(
public void WriteTransformType(
Av1TransformType transformType,
Av1TransformSize transformSize,
bool useReducedTransformSet,
@ -525,7 +525,7 @@ internal class Av1SymbolEncoder : IDisposable
/// </summary>
/// <param name="segmentId">The segment identifier.</param>
/// <param name="context">The context derived from neighboring segment identifiers.</param>
internal void WriteSegmentId(int segmentId, int context)
public void WriteSegmentId(int segmentId, int context)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol(segmentId, this.segmentId[context]);
@ -536,7 +536,7 @@ internal class Av1SymbolEncoder : IDisposable
/// </summary>
/// <param name="skip">Indicates whether the block contains no coded transform coefficients.</param>
/// <param name="context">The neighboring skip context.</param>
internal void WriteSkip(bool skip, int context)
public void WriteSkip(bool skip, int context)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol(skip, this.skip[context]);
@ -547,7 +547,7 @@ internal class Av1SymbolEncoder : IDisposable
/// </summary>
/// <param name="skip">Indicates whether skip mode is selected.</param>
/// <param name="context">The neighboring skip-mode context.</param>
internal void WriteSkipMode(bool skip, int context)
public void WriteSkipMode(bool skip, int context)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol(skip, this.skipMode[context]);
@ -558,7 +558,7 @@ internal class Av1SymbolEncoder : IDisposable
/// </summary>
/// <param name="filterIntraMode">The selected filter-intra mode, or the disabled sentinel.</param>
/// <param name="blockSize">The block size selecting the enable distribution.</param>
internal void WriteFilterIntraMode(Av1FilterIntraMode filterIntraMode, Av1BlockSize blockSize)
public void WriteFilterIntraMode(Av1FilterIntraMode filterIntraMode, Av1BlockSize blockSize)
{
ref Av1SymbolWriter w = ref this.writer;
bool useFilter = filterIntraMode != Av1FilterIntraMode.AllFilterIntraModes;
@ -573,7 +573,7 @@ internal class Av1SymbolEncoder : IDisposable
/// Writes a signed quantizer-index delta value.
/// </summary>
/// <param name="deltaQindex">The signed quantizer-index delta.</param>
internal void WriteDeltaQuantizerIndex(int deltaQindex)
public void WriteDeltaQuantizerIndex(int deltaQindex)
{
ref Av1SymbolWriter w = ref this.writer;
bool sign = deltaQindex < 0;
@ -603,7 +603,7 @@ internal class Av1SymbolEncoder : IDisposable
/// <param name="lumaMode">The luma prediction mode.</param>
/// <param name="topContext">The reduced above-mode context.</param>
/// <param name="leftContext">The reduced left-mode context.</param>
internal void WriteLumaMode(Av1PredictionMode lumaMode, byte topContext, byte leftContext)
public void WriteLumaMode(Av1PredictionMode lumaMode, byte topContext, byte leftContext)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol((int)lumaMode, this.keyFrameYMode[topContext][leftContext]);
@ -614,7 +614,7 @@ internal class Av1SymbolEncoder : IDisposable
/// </summary>
/// <param name="angleDelta">The signed angle delta offset by <see cref="Av1Constants.MaxAngleDelta"/>.</param>
/// <param name="context">The directional prediction mode selecting the distribution.</param>
internal void WriteAngleDelta(int angleDelta, Av1PredictionMode context)
public void WriteAngleDelta(int angleDelta, Av1PredictionMode context)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol(angleDelta, this.angleDelta[context - Av1PredictionMode.Vertical]);
@ -625,7 +625,7 @@ internal class Av1SymbolEncoder : IDisposable
/// </summary>
/// <param name="cdefStrength">The CDEF strength index.</param>
/// <param name="bitCount">The number of signaled bits.</param>
internal void WriteCdefStrength(int cdefStrength, int bitCount)
public void WriteCdefStrength(int cdefStrength, int bitCount)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteLiteral((uint)cdefStrength, bitCount);
@ -637,7 +637,7 @@ internal class Av1SymbolEncoder : IDisposable
/// <param name="chromaMode">The chroma prediction mode.</param>
/// <param name="isChromaFromLumaAllowed">Indicates whether chroma-from-luma is valid for the block.</param>
/// <param name="lumaMode">The block's luma prediction mode.</param>
internal void WriteChromaMode(Av1PredictionMode chromaMode, bool isChromaFromLumaAllowed, Av1PredictionMode lumaMode)
public void WriteChromaMode(Av1ChromaPredictionMode chromaMode, bool isChromaFromLumaAllowed, Av1PredictionMode lumaMode)
{
ref Av1SymbolWriter w = ref this.writer;
int cflAllowed = isChromaFromLumaAllowed ? 1 : 0;
@ -649,7 +649,7 @@ internal class Av1SymbolEncoder : IDisposable
/// </summary>
/// <param name="chromaFromLumaIndex">The packed U/V alpha-magnitude indices.</param>
/// <param name="joinedSign">The joint U/V sign symbol.</param>
internal void WriteChromaFromLumaAlphas(int chromaFromLumaIndex, int joinedSign)
public void WriteChromaFromLumaAlphas(int chromaFromLumaIndex, int joinedSign)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol(joinedSign, this.chromaFromLumaSign);

47
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolReader.cs

@ -52,6 +52,11 @@ internal ref struct Av1SymbolReader
/// </summary>
private int count;
/// <summary>
/// The adjustment that preserves the logical consumed-bit count after the reader enters implicit zero padding.
/// </summary>
private int tellOffset;
/// <summary>
/// Initializes a new instance of the <see cref="Av1SymbolReader"/> struct over one entropy-coded span.
/// </summary>
@ -65,9 +70,40 @@ internal ref struct Av1SymbolReader
this.difference = (1U << (DecoderWindowsSize - 1)) - 1;
this.range = 0x8000;
this.count = -15;
this.tellOffset = 10 - (DecoderWindowsSize - 8);
this.Refill();
}
/// <summary>
/// Validates that range decoding remained within the bounded tile payload and ended at the required trailing-one bit.
/// </summary>
public void ValidateTrailingBits()
{
int consumedBitCount = this.GetConsumedBitCount();
int consumedByteCount = (consumedBitCount + 7) >> 3;
if (consumedByteCount > this.buffer.Length)
{
throw new InvalidImageContentException("The AV1 tile entropy stream is truncated.");
}
// The final consumed byte must contain one trailing-one bit at the range decoder's exact stopping position,
// followed only by zero bits. This is the same bounded-stream check performed after libaom decodes a tile.
int trailingOneBit = 128 >> ((consumedBitCount - 1) & 7);
int trailingBitMask = (trailingOneBit << 1) - 1;
if ((this.buffer[consumedByteCount - 1] & trailingBitMask) != trailingOneBit)
{
throw new InvalidImageContentException("The AV1 tile entropy stream has invalid trailing bits.");
}
for (int i = consumedByteCount; i < this.buffer.Length; i++)
{
if (this.buffer[i] != 0)
{
throw new InvalidImageContentException("The AV1 tile entropy stream has nonzero padding bytes.");
}
}
}
/// <summary>
/// Reads one symbol and adapts its distribution when CDF updates are enabled.
/// </summary>
@ -234,7 +270,9 @@ internal ref struct Av1SymbolReader
if (position >= end)
{
// AV1 range decoding permits the final interval to consume implicit zero padding. A large count models
// that padding without advancing beyond the bounded source span or repeatedly attempting to refill it.
// that padding without repeatedly attempting to refill it. Preserve the previous count in tellOffset so
// the logical position continues past the bounded source and truncated payloads remain detectable.
this.tellOffset += LotsOfBits - cnt;
cnt = LotsOfBits;
}
@ -242,4 +280,11 @@ internal ref struct Av1SymbolReader
this.count = cnt;
this.position = position;
}
/// <summary>
/// Gets the number of entropy bits consumed from the bounded tile payload, including the initial range-coder bit.
/// </summary>
/// <returns>The logical consumed-bit count.</returns>
private readonly int GetConsumedBitCount()
=> (this.position * 8) - this.count + this.tellOffset;
}

15
src/ImageSharp/Formats/Heif/Av1/IAv1TileReader.cs

@ -4,16 +4,19 @@
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Interface for reading of image tiles.
/// Defines tile-payload consumption and completion for one coded AV1 frame.
/// </summary>
internal interface IAv1TileReader
{
/// <summary>
/// Read the information for a single tile.
/// Reads one entropy-coded tile payload into the current frame state.
/// </summary>
/// <param name="tileData">
/// The bytes of encoded data in the bitstream dedicated to this tile.
/// </param>
/// <param name="tileNum">The index of the tile that is to be read.</param>
/// <param name="tileData">The bounded bitstream bytes belonging to the tile.</param>
/// <param name="tileNum">The zero-based tile index in raster order.</param>
void ReadTile(Span<byte> tileData, int tileNum);
/// <summary>
/// Completes the current coded frame after all tile payloads have been read and releases frame-scoped resources.
/// </summary>
void CompleteFrame();
}

234
src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionParameters.cs

@ -0,0 +1,234 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
/// <summary>
/// Stores one AV1 global-motion model in the codec's fixed-point affine matrix domain.
/// </summary>
internal struct Av1GlobalMotionParameters
{
/// <summary>
/// The number of fractional bits carried by every stored matrix parameter.
/// </summary>
public const int ModelPrecisionBits = 16;
/// <summary>
/// The fixed-point representation of one in the global-motion matrix domain.
/// </summary>
public const int ModelScale = 1 << ModelPrecisionBits;
/// <summary>
/// The number of low-order bits removed from the derived shear parameters.
/// </summary>
private const int ShearParameterReductionBits = 6;
/// <summary>
/// The number of fractional bits carried by entries in <see cref="ReciprocalTable"/>.
/// </summary>
private const int ReciprocalPrecisionBits = 14;
/// <summary>
/// The number of divisor-fraction bits used to index <see cref="ReciprocalTable"/>.
/// </summary>
private const int ReciprocalIndexBits = 8;
/// <summary>
/// The six parameters ordered as horizontal translation, vertical translation, and the four affine coefficients.
/// </summary>
private InlineArray6<int> matrix;
/// <summary>
/// Gets an identity global-motion model.
/// </summary>
public static Av1GlobalMotionParameters Identity
{
get
{
Av1GlobalMotionParameters result = default;
result.matrix[2] = ModelScale;
result.matrix[5] = ModelScale;
return result;
}
}
/// <summary>
/// Gets or sets the geometric model represented by the matrix parameters.
/// </summary>
public Av1GlobalMotionType Type { get; set; }
/// <summary>
/// Gets the reduced horizontal scale delta used by warped prediction.
/// </summary>
public short Alpha { get; private set; }
/// <summary>
/// Gets the reduced horizontal shear used by warped prediction.
/// </summary>
public short Beta { get; private set; }
/// <summary>
/// Gets the reduced vertical shear used by warped prediction.
/// </summary>
public short Gamma { get; private set; }
/// <summary>
/// Gets the reduced vertical scale delta used by warped prediction.
/// </summary>
public short Delta { get; private set; }
/// <summary>
/// Gets a value indicating whether the affine model violates AV1's permitted shear bounds.
/// </summary>
public bool IsInvalid { get; private set; }
/// <summary>
/// Gets the fixed-point reciprocal lookup used by AV1's affine shear derivation.
/// </summary>
private static ReadOnlySpan<ushort> ReciprocalTable =>
[
16384, 16320, 16257, 16194, 16132, 16070, 16009, 15948, 15888, 15828, 15768,
15709, 15650, 15592, 15534, 15477, 15420, 15364, 15308, 15252, 15197, 15142,
15087, 15033, 14980, 14926, 14873, 14821, 14769, 14717, 14665, 14614, 14564,
14513, 14463, 14413, 14364, 14315, 14266, 14218, 14170, 14122, 14075, 14028,
13981, 13935, 13888, 13843, 13797, 13752, 13707, 13662, 13618, 13574, 13530,
13487, 13443, 13400, 13358, 13315, 13273, 13231, 13190, 13148, 13107, 13066,
13026, 12985, 12945, 12906, 12866, 12827, 12788, 12749, 12710, 12672, 12633,
12596, 12558, 12520, 12483, 12446, 12409, 12373, 12336, 12300, 12264, 12228,
12193, 12157, 12122, 12087, 12053, 12018, 11984, 11950, 11916, 11882, 11848,
11815, 11782, 11749, 11716, 11683, 11651, 11619, 11586, 11555, 11523, 11491,
11460, 11429, 11398, 11367, 11336, 11305, 11275, 11245, 11215, 11185, 11155,
11125, 11096, 11067, 11038, 11009, 10980, 10951, 10923, 10894, 10866, 10838,
10810, 10782, 10755, 10727, 10700, 10673, 10645, 10618, 10592, 10565, 10538,
10512, 10486, 10460, 10434, 10408, 10382, 10356, 10331, 10305, 10280, 10255,
10230, 10205, 10180, 10156, 10131, 10107, 10082, 10058, 10034, 10010, 9986,
9963, 9939, 9916, 9892, 9869, 9846, 9823, 9800, 9777, 9754, 9732, 9709, 9687,
9664, 9642, 9620, 9598, 9576, 9554, 9533, 9511, 9489, 9468, 9447, 9425, 9404,
9383, 9362, 9341, 9321, 9300, 9279, 9259, 9239, 9218, 9198, 9178, 9158, 9138,
9118, 9098, 9079, 9059, 9039, 9020, 9001, 8981, 8962, 8943, 8924, 8905, 8886,
8867, 8849, 8830, 8812, 8793, 8775, 8756, 8738, 8720, 8702, 8684, 8666, 8648,
8630, 8613, 8595, 8577, 8560, 8542, 8525, 8508, 8490, 8473, 8456, 8439, 8422,
8405, 8389, 8372, 8355, 8339, 8322, 8306, 8289, 8273, 8257, 8240, 8224, 8208,
8192,
];
/// <summary>
/// Gets or sets a matrix parameter in AV1 affine-transform order.
/// </summary>
/// <param name="index">The zero-based matrix parameter index.</param>
/// <returns>The fixed-point matrix parameter.</returns>
public int this[int index]
{
get => this.matrix[index];
set => this.matrix[index] = value;
}
/// <summary>
/// Derives the reduced shear parameters and records whether the complete affine model is valid.
/// </summary>
public void UpdateShearParameters()
{
Span<int> values = this.matrix;
this.Alpha = 0;
this.Beta = 0;
this.Gamma = 0;
this.Delta = 0;
if (values[2] <= 0)
{
this.IsInvalid = true;
return;
}
this.Alpha = (short)Math.Clamp(values[2] - ModelScale, short.MinValue, short.MaxValue);
this.Beta = (short)Math.Clamp(values[3], short.MinValue, short.MaxValue);
// AV1 derives gamma and delta by multiplying with a fixed-precision reciprocal of the horizontal scale.
// The reciprocal lookup is normative; integer division would produce different warped sample positions.
int reciprocal = ResolveDivisor((uint)values[2], out int reciprocalShift);
long scaledVerticalCoefficient = (long)values[4] * ModelScale * reciprocal;
this.Gamma = (short)Math.Clamp(RoundPowerOf2Signed(scaledVerticalCoefficient, reciprocalShift), short.MinValue, short.MaxValue);
long scaledCrossCoefficient = (long)values[3] * values[4] * reciprocal;
long verticalScaleDelta = values[5] - RoundPowerOf2Signed(scaledCrossCoefficient, reciprocalShift) - ModelScale;
this.Delta = (short)Math.Clamp(verticalScaleDelta, short.MinValue, short.MaxValue);
// Warped filtering addresses a coarser parameter grid than the stored affine matrix. Symmetric rounding is
// required here so negative shear values are quantized identically to their positive counterparts.
this.Alpha = ReduceShearParameter(this.Alpha);
this.Beta = ReduceShearParameter(this.Beta);
this.Gamma = ReduceShearParameter(this.Gamma);
this.Delta = ReduceShearParameter(this.Delta);
// These weighted L1 bounds are the AV1 validity test for the two shear axes. Equality is invalid because the
// warped-filter footprint would no longer remain inside the permitted affine sampling envelope.
this.IsInvalid =
((4 * Math.Abs((int)this.Alpha)) + (7 * Math.Abs((int)this.Beta)) >= ModelScale) ||
((4 * Math.Abs((int)this.Gamma)) + (4 * Math.Abs((int)this.Delta)) >= ModelScale);
}
/// <summary>
/// Quantizes one signed shear parameter to AV1's warped-filter precision.
/// </summary>
/// <param name="value">The full-precision shear parameter.</param>
/// <returns>The reduced shear parameter.</returns>
private static short ReduceShearParameter(short value)
=> (short)(RoundPowerOf2Signed(value, ShearParameterReductionBits) * (1 << ShearParameterReductionBits));
/// <summary>
/// Resolves a positive divisor into AV1's fixed-point reciprocal representation.
/// </summary>
/// <param name="divisor">The positive divisor.</param>
/// <param name="shift">Receives the reciprocal's binary scale.</param>
/// <returns>The fixed-point reciprocal multiplier.</returns>
private static int ResolveDivisor(uint divisor, out int shift)
{
// Normalize the divisor around its highest set bit, then quantize the remaining fraction to the normative
// eight-bit table index. Adding the table's fourteen fractional bits yields the scale used by the caller's
// rounded multiply instead of a platform-dependent integer division.
shift = BitOperations.Log2(divisor);
int remainder = (int)(divisor - (1U << shift));
int reciprocalIndex = shift > ReciprocalIndexBits
? RoundPowerOf2(remainder, shift - ReciprocalIndexBits)
: remainder << (ReciprocalIndexBits - shift);
shift += ReciprocalPrecisionBits;
return ReciprocalTable[reciprocalIndex];
}
/// <summary>
/// Divides a nonnegative integer by a power of two with nearest-integer rounding.
/// </summary>
/// <param name="value">The nonnegative value.</param>
/// <param name="bitCount">The base-two divisor exponent.</param>
/// <returns>The rounded quotient.</returns>
private static int RoundPowerOf2(int value, int bitCount)
=> (value + ((1 << bitCount) >> 1)) >> bitCount;
/// <summary>
/// Divides a signed integer by a power of two with symmetric nearest-integer rounding.
/// </summary>
/// <param name="value">The signed value.</param>
/// <param name="bitCount">The base-two divisor exponent.</param>
/// <returns>The rounded quotient.</returns>
private static long RoundPowerOf2Signed(long value, int bitCount)
=> value < 0
? -(((-value) + ((1L << bitCount) >> 1)) >> bitCount)
: (value + ((1L << bitCount) >> 1)) >> bitCount;
/// <summary>
/// Provides inline storage for the six parameters in an AV1 affine matrix.
/// </summary>
/// <typeparam name="T">The stored parameter type.</typeparam>
[InlineArray(6)]
private struct InlineArray6<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
}

30
src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionType.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
/// <summary>
/// Identifies the geometric model carried by AV1 global-motion parameters.
/// </summary>
internal enum Av1GlobalMotionType : byte
{
/// <summary>
/// No geometric displacement is applied.
/// </summary>
Identity = 0,
/// <summary>
/// Horizontal and vertical translation are applied.
/// </summary>
Translation = 1,
/// <summary>
/// Translation, rotation, and uniform zoom are applied.
/// </summary>
RotationZoom = 2,
/// <summary>
/// A general six-parameter affine transformation is applied.
/// </summary>
Affine = 3
}

26
src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopy.cs

@ -36,7 +36,7 @@ internal static class Av1IntraBlockCopy
/// <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,
ref Av1PartitionInfo partitionInfo,
Av1TileInfo tileInfo,
int superblockModeInfoSize,
Span<Av1MotionVector> candidates,
@ -69,17 +69,17 @@ internal static class Av1IntraBlockCopy
int processedColumns = 0;
if (Math.Abs(maximumRowOffset) >= 1)
{
ScanRow(partitionInfo, -1, maximumRowOffset, candidates, weights, ref candidateCount, ref processedRows);
ScanRow(ref partitionInfo, -1, maximumRowOffset, candidates, weights, ref candidateCount, ref processedRows);
}
if (Math.Abs(maximumColumnOffset) >= 1)
{
ScanColumn(partitionInfo, -1, maximumColumnOffset, candidates, weights, ref candidateCount, ref processedColumns);
ScanColumn(ref partitionInfo, -1, maximumColumnOffset, candidates, weights, ref candidateCount, ref processedColumns);
}
if (HasTopRight(partitionInfo, superblockModeInfoSize))
if (HasTopRight(ref partitionInfo, superblockModeInfoSize))
{
AddBlock(partitionInfo, -1, width, tileInfo, candidates, weights, ref candidateCount);
AddBlock(ref partitionInfo, -1, width, tileInfo, candidates, weights, ref candidateCount);
}
int nearestCandidateCount = candidateCount;
@ -90,19 +90,19 @@ internal static class Av1IntraBlockCopy
// 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);
AddBlock(ref 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);
ScanRow(ref 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);
ScanColumn(ref partitionInfo, columnOffset, maximumColumnOffset, candidates, weights, ref candidateCount, ref processedColumns);
}
}
@ -138,7 +138,7 @@ internal static class Av1IntraBlockCopy
/// <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)
public static bool IsValid(Av1MotionVector vector, ref Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo, ObuSequenceHeader sequenceHeader)
{
const int eighthSampleScale = 8;
const int modeInfoSampleSize = 1 << Av1Constants.ModeInfoSizeLog2;
@ -207,7 +207,7 @@ internal static class Av1IntraBlockCopy
/// Scans a mode-information row using AV1's block-size-dependent steps and weights.
/// </summary>
private static void ScanRow(
Av1PartitionInfo partitionInfo,
ref Av1PartitionInfo partitionInfo,
int rowOffset,
int maximumRowOffset,
Span<Av1MotionVector> candidates,
@ -261,7 +261,7 @@ internal static class Av1IntraBlockCopy
/// Scans a mode-information column using AV1's block-size-dependent steps and weights.
/// </summary>
private static void ScanColumn(
Av1PartitionInfo partitionInfo,
ref Av1PartitionInfo partitionInfo,
int columnOffset,
int maximumColumnOffset,
Span<Av1MotionVector> candidates,
@ -315,7 +315,7 @@ internal static class Av1IntraBlockCopy
/// Adds the intra-block-copy vector at one tile-relative search position.
/// </summary>
private static void AddBlock(
Av1PartitionInfo partitionInfo,
ref Av1PartitionInfo partitionInfo,
int rowOffset,
int columnOffset,
Av1TileInfo tileInfo,
@ -400,7 +400,7 @@ internal static class Av1IntraBlockCopy
/// <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)
private static bool HasTopRight(ref Av1PartitionInfo partitionInfo, int superblockModeInfoSize)
{
int width = partitionInfo.ModeInfo.BlockSize.Get4x4WideCount();
int height = partitionInfo.ModeInfo.BlockSize.Get4x4HighCount();

235
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFilmGrainParameters.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
@ -9,167 +11,282 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
internal class ObuFilmGrainParameters
{
/// <summary>
/// Gets or sets a value indicating whether film grain should be added to this frame. A value equal to false specifies that film
/// grain should not be added.
/// Stores the luma scaling-point coordinates without a per-frame array allocation.
/// </summary>
private InlineArray14<byte> pointYValue;
/// <summary>
/// Stores the luma scaling-point values without a per-frame array allocation.
/// </summary>
private InlineArray14<byte> pointYScaling;
/// <summary>
/// Stores the blue-difference scaling-point coordinates without a per-frame array allocation.
/// </summary>
private InlineArray10<byte> pointCbValue;
/// <summary>
/// Stores the blue-difference scaling-point values without a per-frame array allocation.
/// </summary>
private InlineArray10<byte> pointCbScaling;
/// <summary>
/// Stores the red-difference scaling-point coordinates without a per-frame array allocation.
/// </summary>
private InlineArray10<byte> pointCrValue;
/// <summary>
/// Stores the red-difference scaling-point values without a per-frame array allocation.
/// </summary>
private InlineArray10<byte> pointCrScaling;
/// <summary>
/// Stores the luma autoregressive coefficients without a per-frame array allocation.
/// </summary>
private InlineArray24<byte> arCoeffsYPlus128;
/// <summary>
/// Stores the blue-difference autoregressive coefficients without a per-frame array allocation.
/// </summary>
private InlineArray25<byte> arCoeffsCbPlus128;
/// <summary>
/// Stores the red-difference autoregressive coefficients without a per-frame array allocation.
/// </summary>
private InlineArray25<byte> arCoeffsCrPlus128;
/// <summary>
/// Gets or sets a value indicating whether film grain is applied to the displayed frame.
/// </summary>
public bool ApplyGrain { get; set; }
/// <summary>
/// Gets or sets GrainSeed. This value specifies the starting value for the pseudo-random numbers used during film grain synthesis.
/// Gets or sets the 16-bit seed that initializes pseudo-random film-grain synthesis for this frame.
/// </summary>
public uint GrainSeed { get; set; }
/// <summary>
/// Gets or sets a value indicating whether a new set of parameters should be sent. A value equal to false means that the
/// previous set of parameters should be used.
/// Gets or sets a value indicating whether this frame signals a complete parameter set instead of inheriting one.
/// </summary>
public bool UpdateGrain { get; set; }
/// <summary>
/// Gets or sets FilmGrainParamsRefIdx. Indicates which reference frame contains the film grain parameters to be used for this frame.
/// It is a requirement of bitstream conformance that FilmGrainParamsRefIdx is equal to ref_frame_idx[ j ] for some value
/// of j in the range 0 to REFS_PER_FRAME - 1.
/// Gets or sets the physical reference-map index from which this frame inherited its film-grain parameters.
/// The index must match one of the frame's seven selected inter-reference slots.
/// </summary>
public uint FilmGrainParamsRefidx { get; set; }
public uint FilmGrainParamsRefIdx { get; set; }
/// <summary>
/// Gets or sets NumYPoints. Specifies the number of points for the piece-wise linear scaling function of the luma component.
/// It is a requirement of bitstream conformance that NumYPoints is less than or equal to 14.
/// Gets or sets the number of active luma scaling points in the inclusive range zero through fourteen.
/// </summary>
public uint NumYPoints { get; set; }
/// <summary>
/// Gets or sets PointYValue. Represents the x (luma value) coordinate for the i-th point of the piecewise linear scaling function for
/// luma component.The values are signaled on the scale of 0..255. (In case of 10 bit video, these values correspond to
/// luma values divided by 4. In case of 12 bit video, these values correspond to luma values divided by 16.)
///
/// If i is greater than 0, it is a requirement of bitstream conformance that point_y_value[ i ] is greater than point_y_value[ i - 1] (this ensures the x coordinates are specified in increasing order).
/// Gets the fourteen-entry storage for the luma scaling-point coordinates.
/// </summary>
public uint[]? PointYValue { get; set; }
/// <remarks>
/// Only the first <see cref="NumYPoints"/> entries are active. Coordinates use the eight-bit scale and must be
/// strictly increasing; 10-bit and 12-bit sample values are divided by four and sixteen respectively.
/// </remarks>
public Span<byte> PointYValue => this.pointYValue;
/// <summary>
/// Gets or sets PointYScaling. Represents the scaling (output) value for the i-th point of the piecewise linear scaling function for luma component.
/// Gets the fourteen-entry storage for the luma scaling-point output values.
/// </summary>
public uint[]? PointYScaling { get; set; }
/// <remarks>Only the first <see cref="NumYPoints"/> entries are active.</remarks>
public Span<byte> PointYScaling => this.pointYScaling;
/// <summary>
/// Gets or sets a value indicating whether the chroma scaling is inferred from the luma scaling.
/// Gets or sets a value indicating whether both chroma scaling functions are derived from luma samples.
/// </summary>
public bool ChromaScalingFromLuma { get; set; }
/// <summary>
/// Gets or sets NumCbPoints. Specifies the number of points for the piece-wise linear scaling function of the cb component.
/// It is a requirement of bitstream conformance that NumCbPoints is less than or equal to 10.
/// Gets or sets the number of active blue-difference scaling points in the inclusive range zero through ten.
/// </summary>
public uint NumCbPoints { get; set; }
/// <summary>
/// Gets or sets NumCrPoints. Specifies represents the number of points for the piece-wise linear scaling function of the cr component.
/// It is a requirement of bitstream conformance that NumCrPoints is less than or equal to 10.
/// Gets or sets the number of active red-difference scaling points in the inclusive range zero through ten.
/// </summary>
public uint NumCrPoints { get; set; }
/// <summary>
/// Gets or sets PointCbValue. Represents the x coordinate for the i-th point of the piece-wise linear scaling function for cb
/// component.The values are signaled on the scale of 0..255.
/// If i is greater than 0, it is a requirement of bitstream conformance that point_cb_value[ i ] is greater than point_cb_value[ i - 1 ].
/// Gets the ten-entry storage for the blue-difference scaling-point coordinates.
/// </summary>
public uint[]? PointCbValue { get; set; }
/// <remarks>Only the first <see cref="NumCbPoints"/> entries are active, and active coordinates must be strictly increasing.</remarks>
public Span<byte> PointCbValue => this.pointCbValue;
/// <summary>
/// Gets or sets PointCbScaling. Represents the scaling (output) value for the i-th point of the piecewise linear scaling function for cb component.
/// Gets the ten-entry storage for the blue-difference scaling-point output values.
/// </summary>
public uint[]? PointCbScaling { get; set; }
/// <remarks>Only the first <see cref="NumCbPoints"/> entries are active.</remarks>
public Span<byte> PointCbScaling => this.pointCbScaling;
/// <summary>
/// Gets or sets PointCrValue. Represents the x coordinate for the i-th point of the piece-wise linear scaling function for cr component.
/// The values are signaled on the scale of 0..255.
/// If i is greater than 0, it is a requirement of bitstream conformance that point_cr_value[ i ] is greater than point_cr_value[ i - 1 ].
/// Gets the ten-entry storage for the red-difference scaling-point coordinates.
/// </summary>
public uint[]? PointCrValue { get; set; }
/// <remarks>Only the first <see cref="NumCrPoints"/> entries are active, and active coordinates must be strictly increasing.</remarks>
public Span<byte> PointCrValue => this.pointCrValue;
/// <summary>
/// Gets or sets PointCrScaling. Represents the scaling (output) value for the i-th point of the piecewise linear scaling function for cr component.
/// Gets the ten-entry storage for the red-difference scaling-point output values.
/// </summary>
public uint[]? PointCrScaling { get; set; }
/// <remarks>Only the first <see cref="NumCrPoints"/> entries are active.</remarks>
public Span<byte> PointCrScaling => this.pointCrScaling;
/// <summary>
/// Gets or sets GrainScalingMinus8. represents the shift – 8 applied to the values of the chroma component. The
/// grain_scaling_minus_8 can take values of 0..3 and determines the range and quantization step of the standard deviation of film grain.
/// Gets or sets the scaling-function shift minus eight. Values from zero through three select an effective shift
/// from eight through eleven for every luma and chroma scaling value.
/// </summary>
public uint GrainScalingMinus8 { get; set; }
/// <summary>
/// Gets or sets ArCoeffLag. Specifies the number of auto-regressive coefficients for luma and chroma.
/// Gets or sets the autoregressive neighborhood lag in the inclusive range zero through three.
/// </summary>
public uint ArCoeffLag { get; set; }
/// <summary>
/// Gets or sets ArCoeffsYPlus128. Specifies auto-regressive coefficients used for the Y plane.
/// Gets the twenty-four-entry storage for biased luma autoregressive coefficients.
/// </summary>
public uint[]? ArCoeffsYPlus128 { get; set; }
/// <remarks>The active entry count is <c>2 * ArCoeffLag * (ArCoeffLag + 1)</c>.</remarks>
public Span<byte> ArCoeffsYPlus128 => this.arCoeffsYPlus128;
/// <summary>
/// Gets or sets ArCoeffsCbPlus128. Specifies auto-regressive coefficients used for the U plane.
/// Gets the twenty-five-entry storage for biased blue-difference autoregressive coefficients.
/// </summary>
public uint[]? ArCoeffsCbPlus128 { get; set; }
/// <remarks>The active entry count includes one additional luma coefficient when luma scaling points are present.</remarks>
public Span<byte> ArCoeffsCbPlus128 => this.arCoeffsCbPlus128;
/// <summary>
/// Gets or sets ArCoeffsCrPlus128. Specifies auto-regressive coefficients used for the V plane.
/// Gets the twenty-five-entry storage for biased red-difference autoregressive coefficients.
/// </summary>
public uint[]? ArCoeffsCrPlus128 { get; set; }
/// <remarks>The active entry count includes one additional luma coefficient when luma scaling points are present.</remarks>
public Span<byte> ArCoeffsCrPlus128 => this.arCoeffsCrPlus128;
/// <summary>
/// Gets or sets ArCoeffShiftMinus6. Specifies the range of the auto-regressive coefficients. Values of 0, 1, 2, and 3 correspond to the
/// ranges for auto-regressive coefficients of[-2, 2), [-1, 1), [-0.5, 0.5) and [-0.25, 0.25) respectively.
/// Gets or sets the autoregressive coefficient shift minus six in the inclusive range zero through three.
/// </summary>
public uint ArCoeffShiftMinus6 { get; set; }
/// <summary>
/// Gets or sets GrainScaleShift. Specifies how much the Gaussian random numbers should be scaled down during the grain synthesis process.
/// Gets or sets the right shift applied to generated Gaussian grain samples in the inclusive range zero through three.
/// </summary>
public uint GrainScaleShift { get; set; }
/// <summary>
/// Gets or sets CbMult. Represents a multiplier for the cb component used in derivation of the input index to the cb component scaling function.
/// Gets or sets the 8-bit blue-difference sample multiplier used to derive the chroma scaling index.
/// </summary>
public uint CbMult { get; set; }
/// <summary>
/// Gets or sets CbLumaMult. Represents a multiplier for the average luma component used in derivation of the input index to the cb component scaling function.
/// Gets or sets the 8-bit average-luma multiplier used to derive the blue-difference scaling index.
/// </summary>
public uint CbLumaMult { get; set; }
/// <summary>
/// Gets or sets CbOffset. Represents an offset used in derivation of the input index to the cb component scaling function.
/// Gets or sets the 9-bit offset used to derive the blue-difference scaling index.
/// </summary>
public uint CbOffset { get; set; }
/// <summary>
/// Gets or sets CrMult. Represents a multiplier for the cr component used in derivation of the input index to the cr component scaling function.
/// Gets or sets the 8-bit red-difference sample multiplier used to derive the chroma scaling index.
/// </summary>
public uint CrMult { get; set; }
/// <summary>
/// Gets or sets CrLumaMult. Represents a multiplier for the average luma component used in derivation of the input index to the cr component scaling function.
/// Gets or sets the 8-bit average-luma multiplier used to derive the red-difference scaling index.
/// </summary>
public uint CrLumaMult { get; set; }
/// <summary>
/// Gets or sets CrOffset. Represents an offset used in derivation of the input index to the cr component scaling function.
/// Gets or sets the 9-bit offset used to derive the red-difference scaling index.
/// </summary>
public uint CrOffset { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the overlap between film grain blocks shall be applied. OverlapFlag equal to false
/// indicates that the overlap between film grain blocks shall not be applied.
/// Gets or sets a value indicating whether neighboring film-grain blocks are blended across their boundaries.
/// </summary>
public bool OverlapFlag { get; set; }
/// <summary>
/// Gets or sets a value indicating whether clipping to the restricted (studio) range shall be applied to the sample
/// values after adding the film grain(see the semantics for color_range for an explanation of studio swing).
/// ClipToRestrictedRange equal to false indicates that clipping to the full range shall be applied to the sample values after adding the film grain.
/// Gets or sets a value indicating whether grained samples are clipped to the restricted range instead of the full range.
/// </summary>
public bool ClipToRestrictedRange { get; set; }
/// <summary>
/// Replaces the complete film-grain parameter set with values retained by a reference frame.
/// </summary>
/// <param name="source">The retained reference-frame parameters.</param>
public void CopyFrom(ObuFilmGrainParameters source)
{
this.ApplyGrain = source.ApplyGrain;
this.GrainSeed = source.GrainSeed;
this.UpdateGrain = source.UpdateGrain;
this.FilmGrainParamsRefIdx = source.FilmGrainParamsRefIdx;
this.NumYPoints = source.NumYPoints;
this.pointYValue = source.pointYValue;
this.pointYScaling = source.pointYScaling;
this.ChromaScalingFromLuma = source.ChromaScalingFromLuma;
this.NumCbPoints = source.NumCbPoints;
this.NumCrPoints = source.NumCrPoints;
this.pointCbValue = source.pointCbValue;
this.pointCbScaling = source.pointCbScaling;
this.pointCrValue = source.pointCrValue;
this.pointCrScaling = source.pointCrScaling;
this.GrainScalingMinus8 = source.GrainScalingMinus8;
this.ArCoeffLag = source.ArCoeffLag;
this.arCoeffsYPlus128 = source.arCoeffsYPlus128;
this.arCoeffsCbPlus128 = source.arCoeffsCbPlus128;
this.arCoeffsCrPlus128 = source.arCoeffsCrPlus128;
this.ArCoeffShiftMinus6 = source.ArCoeffShiftMinus6;
this.GrainScaleShift = source.GrainScaleShift;
this.CbMult = source.CbMult;
this.CbLumaMult = source.CbLumaMult;
this.CbOffset = source.CbOffset;
this.CrMult = source.CrMult;
this.CrLumaMult = source.CrLumaMult;
this.CrOffset = source.CrOffset;
this.OverlapFlag = source.OverlapFlag;
this.ClipToRestrictedRange = source.ClipToRestrictedRange;
}
/// <summary>
/// Provides inline storage for the maximum luma autoregressive coefficient count.
/// </summary>
/// <typeparam name="T">The stored value type.</typeparam>
[InlineArray(24)]
private struct InlineArray24<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
/// <summary>
/// Provides inline storage for the ten scaling points permitted on either chroma plane.
/// </summary>
/// <typeparam name="T">The stored value type.</typeparam>
[InlineArray(10)]
private struct InlineArray10<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
/// <summary>
/// Provides inline storage for the maximum autoregressive coefficient count of either chroma plane.
/// </summary>
/// <typeparam name="T">The stored value type.</typeparam>
[InlineArray(25)]
private struct InlineArray25<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
}

172
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs

@ -1,6 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
@ -21,10 +24,30 @@ internal class ObuFrameHeader
private InlineArray8<uint> referenceOrderHint;
/// <summary>
/// Stores the reference-map index selected for each of the eight inter references.
/// Stores the frame identifier associated with each of the eight reference-map slots.
/// </summary>
private InlineArray8<uint> referenceFrameId;
/// <summary>
/// Stores the reference-map slot selected for each of the seven inter reference types.
/// </summary>
private InlineArray8<uint> referenceFrameIndex;
/// <summary>
/// Stores the global-motion model associated with each of the seven inter reference types.
/// </summary>
private InlineArray7<Av1GlobalMotionParameters> globalMotionParameters;
/// <summary>
/// Gets or sets the temporal-layer identifier carried by the primary frame-header OBU.
/// </summary>
public int TemporalId { get; set; }
/// <summary>
/// Gets or sets the spatial-layer identifier carried by the primary frame-header OBU.
/// </summary>
public int SpatialId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether motion vectors use integer-sample precision.
/// </summary>
@ -45,6 +68,16 @@ internal class ObuFrameHeader
/// </summary>
public bool AllowHighPrecisionMotionVector { get; set; }
/// <summary>
/// Gets or sets the frame-level interpolation filter used for inter prediction.
/// </summary>
public Av1InterpolationFilter InterpolationFilter { get; set; }
/// <summary>
/// Gets or sets a value indicating whether inter blocks may select a non-translational motion mode.
/// </summary>
public bool IsMotionModeSwitchable { get; set; }
/// <summary>
/// Gets or sets the decoded tile layout.
/// </summary>
@ -148,103 +181,172 @@ internal class ObuFrameHeader
/// <summary>
/// Gets or sets the decoded and rendered frame dimensions.
/// </summary>
internal ObuFrameSize FrameSize { get; set; } = new ObuFrameSize();
public ObuFrameSize FrameSize { get; set; } = new ObuFrameSize();
/// <summary>
/// Gets or sets the frame width in mode-information units.
/// </summary>
internal int ModeInfoColumnCount { get; set; }
public int ModeInfoColumnCount { get; set; }
/// <summary>
/// Gets or sets the frame height in mode-information units.
/// </summary>
internal int ModeInfoRowCount { get; set; }
public int ModeInfoRowCount { get; set; }
/// <summary>
/// Gets or sets a value indicating whether an existing reference frame is displayed without decoding a new frame.
/// </summary>
internal bool ShowExistingFrame { get; set; }
public bool ShowExistingFrame { get; set; }
/// <summary>
/// Gets or sets the coded frame type.
/// </summary>
internal ObuFrameType FrameType { get; set; }
/// <summary>
/// Gets the validity state of each reference-frame slot.
/// </summary>
/// <returns>The mutable eight-entry reference-validity table.</returns>
public Span<bool> GetReferenceValidity() => this.referenceValid;
/// <summary>
/// Gets the multi-bit order hint associated with each reference-frame slot.
/// </summary>
/// <returns>The mutable eight-entry reference-order-hint table.</returns>
public Span<uint> GetReferenceOrderHints() => this.referenceOrderHint;
public ObuFrameType FrameType { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the decoded frame is immediately displayed.
/// </summary>
internal bool ShowFrame { get; set; }
public bool ShowFrame { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the frame may be displayed by a later header.
/// </summary>
internal bool ShowableFrame { get; set; }
public bool ShowableFrame { get; set; }
/// <summary>
/// Gets or sets the reference map index selected when showing an existing frame.
/// </summary>
internal uint FrameToShowMapIdx { get; set; }
public uint FrameToShowMapIdx { get; set; }
/// <summary>
/// Gets or sets the display frame identifier.
/// </summary>
internal uint DisplayFrameId { get; set; }
public uint DisplayFrameId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the frame can be decoded without state from earlier frames.
/// </summary>
internal bool ErrorResilientMode { get; set; }
public bool ErrorResilientMode { get; set; }
/// <summary>
/// Gets or sets a value indicating whether screen-content coding tools are permitted.
/// </summary>
internal bool AllowScreenContentTools { get; set; }
public bool AllowScreenContentTools { get; set; }
/// <summary>
/// Gets or sets a value indicating whether probability adaptation is disabled for this frame.
/// </summary>
internal bool DisableCdfUpdate { get; set; }
public bool DisableCdfUpdate { get; set; }
/// <summary>
/// Gets or sets the identifier of the current frame.
/// </summary>
internal uint CurrentFrameId { get; set; }
public uint CurrentFrameId { get; set; }
/// <summary>
/// Gets the reference-map index selected for each inter reference.
/// Gets or sets the frame order hint.
/// </summary>
/// <returns>The mutable eight-entry reference-frame-index table.</returns>
public Span<uint> GetReferenceFrameIndices() => this.referenceFrameIndex;
public uint OrderHint { get; set; }
/// <summary>
/// Gets or sets the frame order hint.
/// Gets or sets the zero-based inter-reference type that supplies the initial frame context, or the no-reference
/// sentinel.
/// </summary>
internal uint OrderHint { get; set; }
public uint PrimaryReferenceFrame { get; set; } = Av1Constants.PrimaryReferenceFrameNone;
/// <summary>
/// Gets or sets the reference frame supplying the initial probability context.
/// Gets or sets the resolved reference-map slot supplying the initial frame context, or <see langword="null"/> when
/// the frame uses the default context.
/// </summary>
internal uint PrimaryReferenceFrame { get; set; } = Av1Constants.PrimaryReferenceFrameNone;
public byte? PrimaryReferenceSlot { get; set; }
/// <summary>
/// Gets or sets the bit mask of reference slots refreshed by this frame.
/// </summary>
internal uint RefreshFrameFlags { get; set; }
public uint RefreshFrameFlags { get; set; }
/// <summary>
/// Gets or sets the presentation time signaled by temporal point information.
/// </summary>
internal uint FramePresentationTime { get; set; }
public uint FramePresentationTime { get; set; }
/// <summary>
/// Gets the validity state of each reference-frame slot.
/// </summary>
/// <returns>The mutable eight-entry reference-validity table.</returns>
public Span<bool> GetReferenceValidity() => this.referenceValid;
/// <summary>
/// Gets the multi-bit order hint associated with each reference-frame slot.
/// </summary>
/// <returns>The mutable eight-entry reference-order-hint table.</returns>
public Span<uint> GetReferenceOrderHints() => this.referenceOrderHint;
/// <summary>
/// Gets the frame identifier associated with each reference-map slot.
/// </summary>
/// <returns>The mutable eight-entry reference-frame-identifier table.</returns>
public Span<uint> GetReferenceFrameIds() => this.referenceFrameId;
/// <summary>
/// Gets the reference-map slot selected for each inter reference type.
/// </summary>
/// <returns>The mutable seven-entry inter-reference-map table.</returns>
public Span<uint> GetReferenceFrameIndices() => this.referenceFrameIndex[..Av1Constants.ReferencesPerFrame];
/// <summary>
/// Gets the global-motion model associated with each canonical inter reference type from LAST through ALTREF.
/// </summary>
/// <returns>The mutable seven-entry global-motion parameter table.</returns>
public Span<Av1GlobalMotionParameters> GetGlobalMotionParameters() => this.globalMotionParameters;
/// <summary>
/// Invalidates retained reference slots whose frame identifiers fall outside the permitted backwards window.
/// </summary>
/// <param name="frameIdLength">The number of bits in the modulo frame-identifier domain.</param>
/// <param name="deltaFrameIdLength">The number of bits used to signal reference-frame identifier deltas.</param>
public void MarkReferenceFrames(int frameIdLength, int deltaFrameIdLength)
{
uint referenceWindow = 1U << deltaFrameIdLength;
uint frameIdModulus = 1U << frameIdLength;
Span<uint> referenceFrameIds = this.GetReferenceFrameIds();
Span<bool> referenceValidity = this.GetReferenceValidity();
for (int slot = 0; slot < Av1Constants.ReferenceFrameCount; slot++)
{
uint referenceFrameId = referenceFrameIds[slot];
if (this.CurrentFrameId > referenceWindow)
{
// Without wraparound, a retained identifier is valid only in the closed interval ending at the
// current identifier and extending referenceWindow values backwards.
if (referenceFrameId > this.CurrentFrameId || referenceFrameId < this.CurrentFrameId - referenceWindow)
{
referenceValidity[slot] = false;
}
}
else
{
// When the backwards window crosses zero, valid identifiers occupy both ends of the modulo domain.
// Only the open interval between the current identifier and the wrapped lower bound is invalid.
uint wrappedLowerBound = frameIdModulus + this.CurrentFrameId - referenceWindow;
if (referenceFrameId > this.CurrentFrameId && referenceFrameId < wrappedLowerBound)
{
referenceValidity[slot] = false;
}
}
}
}
/// <summary>
/// Provides inline storage for the seven canonical AV1 inter reference types.
/// </summary>
/// <typeparam name="T">The stored parameter type.</typeparam>
[InlineArray(Av1Constants.ReferencesPerFrame)]
private struct InlineArray7<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
}

101
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameReferenceState.cs

@ -0,0 +1,101 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Stores the uncompressed-header reference state retained by one AV1 OBU reader session.
/// </summary>
/// <remarks>
/// This state describes the eight reference-map slots but does not own reconstructed sample buffers. Pixel ownership
/// remains with the decoder's reference-frame store and is committed before this syntax state is completed. CDF,
/// segmentation, loop-filter, motion, and layer metadata remain on that retained frame owner; the current header's
/// resolved primary-reference slot selects the shared owner instead of duplicating those values here.
/// </remarks>
internal struct ObuFrameReferenceState
{
/// <summary>
/// Stores whether each of the eight reference-map slots can be selected by a later frame.
/// </summary>
private InlineArray8<bool> referenceValidity;
/// <summary>
/// Stores the frame identifier associated with each of the eight reference-map slots.
/// </summary>
private InlineArray8<uint> referenceFrameIds;
/// <summary>
/// Stores the order hint associated with each of the eight reference-map slots.
/// </summary>
private InlineArray8<uint> referenceOrderHints;
/// <summary>
/// Gets a value indicating whether a completed frame identifier is available for the next header.
/// </summary>
public bool HasCurrentFrameId { get; private set; }
/// <summary>
/// Gets the frame identifier of the most recently completed frame.
/// </summary>
public uint CurrentFrameId { get; private set; }
/// <summary>
/// Copies the completed reference-map state into a newly created frame header.
/// </summary>
/// <param name="frameHeader">The frame header that will parse and derive state from the retained map.</param>
public void InitializeFrameHeader(ObuFrameHeader frameHeader)
{
ReadOnlySpan<bool> referenceValidity = this.referenceValidity;
ReadOnlySpan<uint> referenceFrameIds = this.referenceFrameIds;
ReadOnlySpan<uint> referenceOrderHints = this.referenceOrderHints;
// Only the eight retained-slot tables cross a frame boundary. The seven inter-reference roles are signaled or
// derived afresh for each frame, and the primary context source is resolved from that per-frame mapping.
referenceValidity.CopyTo(frameHeader.GetReferenceValidity());
referenceFrameIds.CopyTo(frameHeader.GetReferenceFrameIds());
referenceOrderHints.CopyTo(frameHeader.GetReferenceOrderHints());
}
/// <summary>
/// Publishes the reference-map transition produced by a successfully completed frame.
/// </summary>
/// <param name="frameHeader">The completed frame header whose refresh mask selects the replaced slots.</param>
/// <param name="frameIdNumbersPresent">
/// A value indicating whether the sequence carries modulo frame identifiers.
/// </param>
public void CompleteFrame(ObuFrameHeader frameHeader, bool frameIdNumbersPresent)
{
Span<bool> referenceValidity = frameHeader.GetReferenceValidity();
Span<uint> referenceFrameIds = frameHeader.GetReferenceFrameIds();
Span<uint> referenceOrderHints = frameHeader.GetReferenceOrderHints();
// Refresh is published only at this successful completion boundary. Updating the completed header first keeps
// the same object retained by the reconstructed frame owner synchronized with the next parser-session snapshot.
for (int slot = 0; slot < Av1Constants.ReferenceFrameCount; slot++)
{
if ((frameHeader.RefreshFrameFlags & (1U << slot)) != 0)
{
referenceValidity[slot] = true;
referenceFrameIds[slot] = frameHeader.CurrentFrameId;
referenceOrderHints[slot] = frameHeader.OrderHint;
}
}
referenceValidity.CopyTo(this.referenceValidity);
referenceFrameIds.CopyTo(this.referenceFrameIds);
referenceOrderHints.CopyTo(this.referenceOrderHints);
if (frameIdNumbersPresent)
{
// libaom keeps one current_frame_id in decoder-session state. The following header snapshots this value as
// its previous identifier before consuming its own current_frame_id syntax.
this.CurrentFrameId = frameHeader.CurrentFrameId;
this.HasCurrentFrameId = true;
}
}
/// <summary>
/// Clears the completed frame identifier and every retained reference-map slot.
/// </summary>
public void Reset() => this = default;
}

12
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameSize.cs

@ -11,30 +11,30 @@ internal class ObuFrameSize
/// <summary>
/// Gets or sets the coded frame width.
/// </summary>
internal int FrameWidth { get; set; }
public int FrameWidth { get; set; }
/// <summary>
/// Gets or sets the coded frame height.
/// </summary>
internal int FrameHeight { get; set; }
public int FrameHeight { get; set; }
/// <summary>
/// Gets or sets the denominator used by AV1 super-resolution scaling.
/// </summary>
internal int SuperResolutionDenominator { get; set; }
public int SuperResolutionDenominator { get; set; }
/// <summary>
/// Gets or sets the frame width after super-resolution upscaling.
/// </summary>
internal int SuperResolutionUpscaledWidth { get; set; }
public int SuperResolutionUpscaledWidth { get; set; }
/// <summary>
/// Gets or sets the intended display width.
/// </summary>
internal int RenderWidth { get; set; }
public int RenderWidth { get; set; }
/// <summary>
/// Gets or sets the intended display height.
/// </summary>
internal int RenderHeight { get; set; }
public int RenderHeight { get; set; }
}

8
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOrderHintInfo.cs

@ -11,20 +11,20 @@ internal class ObuOrderHintInfo
/// <summary>
/// Gets or sets a value indicating whether order hints are enabled.
/// </summary>
public bool EnableOrderHint { get; internal set; }
public bool EnableOrderHint { get; set; }
/// <summary>
/// Gets or sets a value indicating whether joint compound prediction is enabled.
/// </summary>
internal bool EnableJointCompound { get; set; }
public bool EnableJointCompound { get; set; }
/// <summary>
/// Gets or sets a value indicating whether reference-frame motion vectors are enabled.
/// </summary>
internal bool EnableReferenceFrameMotionVectors { get; set; }
public bool EnableReferenceFrameMotionVectors { get; set; }
/// <summary>
/// Gets or sets the number of bits used to encode order hints.
/// </summary>
internal int OrderHintBits { get; set; }
public int OrderHintBits { get; set; }
}

797
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs

File diff suppressed because it is too large

20
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSegmentationParameters.cs

@ -59,6 +59,24 @@ internal class ObuSegmentationParameters
/// <param name="segmentId">The segment identifier.</param>
/// <param name="feature">The feature to inspect.</param>
/// <returns><see langword="true"/> when the feature is active; otherwise, <see langword="false"/>.</returns>
internal bool IsFeatureActive(int segmentId, ObuSegmentationLevelFeature feature)
public bool IsFeatureActive(int segmentId, ObuSegmentationLevelFeature feature)
=> this.FeatureEnabled[segmentId, (int)feature];
/// <summary>
/// Replaces every feature enable flag and value with state from a primary reference frame.
/// </summary>
/// <param name="source">The primary-reference segmentation state.</param>
public void CopyFeaturesFrom(ObuSegmentationParameters source)
{
// AV1 inherits feature data but not the current frame's enabled or update flags. Both dimensions are fixed by
// the bitstream syntax, and copying values into this header prevents retained frames from sharing mutable state.
for (int segment = 0; segment < Av1Constants.MaxSegmentCount; segment++)
{
for (int feature = 0; feature < Av1Constants.SegmentationLevelMax; feature++)
{
this.FeatureEnabled[segment, feature] = source.FeatureEnabled[segment, feature];
this.FeatureData[segment, feature] = source.FeatureData[segment, feature];
}
}
}
}

9
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs

@ -858,8 +858,6 @@ internal class ObuWriter
writer.WriteLiteral(grainParams.GrainSeed, 16);
writer.WriteLiteral(grainParams.NumYPoints, 4);
Guard.NotNull(grainParams.PointYValue);
Guard.NotNull(grainParams.PointYScaling);
for (int i = 0; i < grainParams.NumYPoints; i++)
{
writer.WriteLiteral(grainParams.PointYValue[i], 8);
@ -876,8 +874,6 @@ internal class ObuWriter
(!sequenceHeader.ColorConfig.SubSamplingX || !sequenceHeader.ColorConfig.SubSamplingY || grainParams.NumYPoints != 0))
{
writer.WriteLiteral(grainParams.NumCbPoints, 4);
Guard.NotNull(grainParams.PointCbValue);
Guard.NotNull(grainParams.PointCbScaling);
for (int i = 0; i < grainParams.NumCbPoints; i++)
{
writer.WriteLiteral(grainParams.PointCbValue[i], 8);
@ -885,8 +881,6 @@ internal class ObuWriter
}
writer.WriteLiteral(grainParams.NumCrPoints, 4);
Guard.NotNull(grainParams.PointCrValue);
Guard.NotNull(grainParams.PointCrScaling);
for (int i = 0; i < grainParams.NumCrPoints; i++)
{
writer.WriteLiteral(grainParams.PointCrValue[i], 8);
@ -902,7 +896,6 @@ internal class ObuWriter
if (grainParams.NumYPoints != 0)
{
numPosChroma++;
Guard.NotNull(grainParams.ArCoeffsYPlus128);
for (int i = 0; i < numPosLuma; i++)
{
writer.WriteLiteral(grainParams.ArCoeffsYPlus128[i], 8);
@ -911,7 +904,6 @@ internal class ObuWriter
if (grainParams.ChromaScalingFromLuma || grainParams.NumCbPoints != 0)
{
Guard.NotNull(grainParams.ArCoeffsCbPlus128);
for (int i = 0; i < numPosChroma; i++)
{
writer.WriteLiteral(grainParams.ArCoeffsCbPlus128[i], 8);
@ -920,7 +912,6 @@ internal class ObuWriter
if (grainParams.ChromaScalingFromLuma || grainParams.NumCrPoints != 0)
{
Guard.NotNull(grainParams.ArCoeffsCrPlus128);
for (int i = 0; i < numPosChroma; i++)
{
writer.WriteLiteral(grainParams.ArCoeffsCrPlus128[i], 8);

12
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs

@ -3,7 +3,6 @@
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.FilmGrain;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
@ -14,7 +13,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
/// <summary>
/// Reconstructs the coded blocks of one AV1 still-image frame into planar sample buffers.
/// Reconstructs the coded blocks of one AV1 image frame into planar sample buffers.
/// </summary>
internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable
{
@ -132,13 +131,8 @@ internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable
loopRestorationDecoder.DecodeFrame();
}
// Film grain belongs to the displayed image rather than the reference reconstruction, so it
// follows every in-loop filter. This decoder owns no retained reference frames.
Av1FilmGrainDecoder filmGrainDecoder = new(this.sequenceHeader, this.frameHeader, this.frameBuffer);
filmGrainDecoder.DecodeFrame();
// Extending reference-frame borders is sequence playback state and is deliberately outside
// this still-image decoder's scope.
// Film grain is deliberately excluded here because this buffer is the normative post-restoration reference.
// The owning decoder applies grain only to the presentation buffer after reference ownership is established.
}
/// <summary>

35
src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainDecoder.cs

@ -907,7 +907,7 @@ internal sealed class Av1FilmGrainDecoder
int roundingOffset = 1 << (coefficientShift - 1);
int grainMinimum = -(1 << (bitDepth - 1));
int grainMaximum = (1 << (bitDepth - 1)) - 1;
uint[] coefficients = parameters.ArCoeffsYPlus128!;
ReadOnlySpan<byte> coefficients = parameters.ArCoeffsYPlus128;
// TemplatePadding leaves every lag-one through lag-three predecessor addressable without a boundary branch.
// Raster order guarantees that all rows above and all samples to the left have already been filtered.
@ -1005,8 +1005,8 @@ internal sealed class Av1FilmGrainDecoder
int roundingOffset = 1 << (coefficientShift - 1);
int grainMinimum = -(1 << (bitDepth - 1));
int grainMaximum = (1 << (bitDepth - 1)) - 1;
uint[]? cbCoefficients = parameters.ArCoeffsCbPlus128;
uint[]? crCoefficients = parameters.ArCoeffsCrPlus128;
ReadOnlySpan<byte> cbCoefficients = parameters.ArCoeffsCbPlus128;
ReadOnlySpan<byte> crCoefficients = parameters.ArCoeffsCrPlus128;
// Cb and Cr share the same causal predecessor walk, so both accumulators advance one coefficient index
// together. A disabled plane stays zero but does not alter the coefficient ordering of the enabled plane.
@ -1024,12 +1024,12 @@ internal sealed class Av1FilmGrainDecoder
int grainIndex = ((row + relativeRow) * stride) + column + relativeColumn;
if (applyCb)
{
weightedCb += ((int)cbCoefficients![coefficientIndex] - 128) * cbGrain[grainIndex];
weightedCb += ((int)cbCoefficients[coefficientIndex] - 128) * cbGrain[grainIndex];
}
if (applyCr)
{
weightedCr += ((int)crCoefficients![coefficientIndex] - 128) * crGrain[grainIndex];
weightedCr += ((int)crCoefficients[coefficientIndex] - 128) * crGrain[grainIndex];
}
coefficientIndex++;
@ -1041,12 +1041,12 @@ internal sealed class Av1FilmGrainDecoder
int grainIndex = (row * stride) + column + relativeColumn;
if (applyCb)
{
weightedCb += ((int)cbCoefficients![coefficientIndex] - 128) * cbGrain[grainIndex];
weightedCb += ((int)cbCoefficients[coefficientIndex] - 128) * cbGrain[grainIndex];
}
if (applyCr)
{
weightedCr += ((int)crCoefficients![coefficientIndex] - 128) * crGrain[grainIndex];
weightedCr += ((int)crCoefficients[coefficientIndex] - 128) * crGrain[grainIndex];
}
coefficientIndex++;
@ -1072,12 +1072,12 @@ internal sealed class Av1FilmGrainDecoder
averageLuma = (averageLuma + ((1 << averagingShift) >> 1)) >> averagingShift;
if (applyCb)
{
weightedCb += ((int)cbCoefficients![coefficientIndex] - 128) * averageLuma;
weightedCb += ((int)cbCoefficients[coefficientIndex] - 128) * averageLuma;
}
if (applyCr)
{
weightedCr += ((int)crCoefficients![coefficientIndex] - 128) * averageLuma;
weightedCr += ((int)crCoefficients[coefficientIndex] - 128) * averageLuma;
}
}
@ -1141,8 +1141,8 @@ internal sealed class Av1FilmGrainDecoder
/// <param name="pointCount">The number of populated control points.</param>
/// <param name="lookup">The destination scaling lookup table.</param>
private static void InitializeScalingFunction(
uint[]? pointValues,
uint[]? pointScalings,
ReadOnlySpan<byte> pointValues,
ReadOnlySpan<byte> pointScalings,
int pointCount,
Span<int> lookup)
{
@ -1151,27 +1151,24 @@ internal sealed class Av1FilmGrainDecoder
return;
}
uint[] values = pointValues!;
uint[] scalings = pointScalings!;
// Values outside the first and last control points extend their nearest endpoint rather than extrapolating.
lookup[..(int)values[0]].Fill((int)scalings[0]);
lookup[..(int)pointValues[0]].Fill((int)pointScalings[0]);
for (int point = 0; point < pointCount - 1; point++)
{
int deltaY = (int)scalings[point + 1] - (int)scalings[point];
int deltaX = (int)values[point + 1] - (int)values[point];
int deltaY = (int)pointScalings[point + 1] - (int)pointScalings[point];
int deltaX = (int)pointValues[point + 1] - (int)pointValues[point];
// A rounded Q16 reciprocal performs the piecewise-linear interpolation using integer arithmetic. The
// 32768 bias below rounds each reconstructed scaling value when it returns to integer precision.
long delta = deltaY * ((65536 + (deltaX >> 1)) / deltaX);
for (int x = 0; x < deltaX; x++)
{
lookup[(int)values[point] + x] = (int)scalings[point] +
lookup[(int)pointValues[point] + x] = (int)pointScalings[point] +
(int)(((x * delta) + 32768) >> 16);
}
}
lookup[(int)values[pointCount - 1]..].Fill((int)scalings[pointCount - 1]);
lookup[(int)pointValues[pointCount - 1]..].Fill((int)pointScalings[pointCount - 1]);
}
/// <summary>

2
src/ImageSharp/Formats/Heif/Av1/Pipeline/IAv1FrameDecoder.cs

@ -6,7 +6,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
/// <summary>
/// Defines reconstruction of decoded AV1 superblocks within a single still-image frame.
/// Defines reconstruction of decoded AV1 superblocks within one coded image frame.
/// </summary>
internal interface IAv1FrameDecoder
{

90
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1ChromaPredictionMode.cs

@ -0,0 +1,90 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// <summary>
/// Identifies the intra prediction mode used by the chroma planes of an AV1 coding block.
/// </summary>
internal enum Av1ChromaPredictionMode : byte
{
/// <summary>
/// Predicts each sample from the average of the available top and left neighbors.
/// </summary>
DC,
/// <summary>
/// Repeats the top neighboring row vertically through the block.
/// </summary>
Vertical,
/// <summary>
/// Repeats the left neighboring column horizontally through the block.
/// </summary>
Horizontal,
/// <summary>
/// Projects neighboring samples into the block at 45 degrees.
/// </summary>
Directional45Degrees,
/// <summary>
/// Projects neighboring samples into the block at 135 degrees.
/// </summary>
Directional135Degrees,
/// <summary>
/// Projects neighboring samples into the block at 113 degrees.
/// </summary>
Directional113Degrees,
/// <summary>
/// Projects neighboring samples into the block at 157 degrees.
/// </summary>
Directional157Degrees,
/// <summary>
/// Projects neighboring samples into the block at 203 degrees.
/// </summary>
Directional203Degrees,
/// <summary>
/// Projects neighboring samples into the block at 67 degrees.
/// </summary>
Directional67Degrees,
/// <summary>
/// Blends horizontal and vertical smooth predictions.
/// </summary>
Smooth,
/// <summary>
/// Interpolates vertically between the top row and the bottom-left neighbor.
/// </summary>
SmoothVertical,
/// <summary>
/// Interpolates horizontally between the left column and the top-right neighbor.
/// </summary>
SmoothHorizontal,
/// <summary>
/// Selects the neighbor with the smallest gradient from the top-left reference.
/// </summary>
Paeth,
/// <summary>
/// Predicts chroma from the reconstructed luma AC surface.
/// </summary>
ChromaFromLuma,
/// <summary>
/// The exclusive upper bound of valid chroma intra-prediction modes.
/// </summary>
ModeCount,
/// <summary>
/// Identifies an unavailable chroma prediction mode on an inter-predicted block.
/// </summary>
Invalid,
}

46
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1ChromaPredictionModeExtensions.cs

@ -0,0 +1,46 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// <summary>
/// Provides luma-equivalent prediction metadata for AV1 chroma intra-prediction modes.
/// </summary>
internal static class Av1ChromaPredictionModeExtensions
{
/// <summary>
/// Maps a chroma intra-prediction mode to the equivalent luma intra-prediction mode.
/// </summary>
/// <param name="mode">The chroma intra-prediction mode.</param>
/// <returns>The luma mode with the same spatial predictor, or the invalid luma sentinel for an invalid chroma mode.</returns>
public static Av1PredictionMode ToLumaMode(this Av1ChromaPredictionMode mode)
=> mode switch
{
Av1ChromaPredictionMode.DC => Av1PredictionMode.DC,
Av1ChromaPredictionMode.Vertical => Av1PredictionMode.Vertical,
Av1ChromaPredictionMode.Horizontal => Av1PredictionMode.Horizontal,
Av1ChromaPredictionMode.Directional45Degrees => Av1PredictionMode.Directional45Degrees,
Av1ChromaPredictionMode.Directional135Degrees => Av1PredictionMode.Directional135Degrees,
Av1ChromaPredictionMode.Directional113Degrees => Av1PredictionMode.Directional113Degrees,
Av1ChromaPredictionMode.Directional157Degrees => Av1PredictionMode.Directional157Degrees,
Av1ChromaPredictionMode.Directional203Degrees => Av1PredictionMode.Directional203Degrees,
Av1ChromaPredictionMode.Directional67Degrees => Av1PredictionMode.Directional67Degrees,
Av1ChromaPredictionMode.Smooth => Av1PredictionMode.Smooth,
Av1ChromaPredictionMode.SmoothVertical => Av1PredictionMode.SmoothVertical,
Av1ChromaPredictionMode.SmoothHorizontal => Av1PredictionMode.SmoothHorizontal,
Av1ChromaPredictionMode.Paeth => Av1PredictionMode.Paeth,
// Chroma-from-luma adds its AC contribution to a DC prediction. libaom's get_uv_mode() therefore maps it
// to DC when shared transform and neighbor metadata require the corresponding luma predictor.
Av1ChromaPredictionMode.ChromaFromLuma => Av1PredictionMode.DC,
_ => Av1PredictionMode.IntraInvalid,
};
/// <summary>
/// Determines whether a chroma intra-prediction mode projects samples along a coded angle.
/// </summary>
/// <param name="mode">The chroma intra-prediction mode.</param>
/// <returns><see langword="true"/> for a directional mode; otherwise, <see langword="false"/>.</returns>
public static bool IsDirectional(this Av1ChromaPredictionMode mode)
=> mode is >= Av1ChromaPredictionMode.Vertical and <= Av1ChromaPredictionMode.Directional67Degrees;
}

16
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.Operator.cs

@ -13,6 +13,22 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// </content>
internal abstract partial class Av1FilterIntraPredictorBase
{
/// <summary>
/// Defines the coefficient set for one AV1 filter-intra prediction mode.
/// </summary>
internal interface IAv1FilterIntraPredictionOperator
{
/// <summary>
/// Gets the filter-intra mode implemented by the operator.
/// </summary>
public static abstract Av1FilterIntraMode Mode { get; }
/// <summary>
/// Gets the eight seven-tap coefficient rows used by the operator.
/// </summary>
public static abstract ReadOnlySpan<sbyte> Taps { get; }
}
/// <summary>
/// Applies one closed filter-intra coefficient operator using the widest useful SIMD width.
/// </summary>

25
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.OperatorContract.cs

@ -1,25 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
internal abstract partial class Av1FilterIntraPredictorBase
{
/// <summary>
/// Defines the coefficient set for one AV1 filter-intra prediction mode.
/// </summary>
internal interface IAv1FilterIntraPredictionOperator
{
/// <summary>
/// Gets the filter-intra mode implemented by the operator.
/// </summary>
public static abstract Av1FilterIntraMode Mode { get; }
/// <summary>
/// Gets the eight seven-tap coefficient rows used by the operator.
/// </summary>
public static abstract ReadOnlySpan<sbyte> Taps { get; }
}
}

174
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.Operator.cs

@ -8,10 +8,182 @@ using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// <content>
/// Provides the width-progressive SIMD traversal shared by closed non-directional prediction operators.
/// Defines the neighbor-usage flags and scalar/SIMD contract for closed intra-prediction operators, and provides
/// their shared width-progressive SIMD traversal.
/// </content>
internal abstract partial class Av1IntraPredictorBase
{
/// <summary>
/// Identifies the neighboring inputs consumed by an AV1 intra-prediction operator.
/// </summary>
[Flags]
internal enum Av1IntraPredictionInputs
{
/// <summary>
/// The operator does not consume neighboring samples.
/// </summary>
None = 0,
/// <summary>
/// The operator consumes samples from the top reference.
/// </summary>
Top = 1,
/// <summary>
/// The operator consumes samples from the left reference.
/// </summary>
Left = 2,
/// <summary>
/// The operator consumes the shared top-left reference.
/// </summary>
TopLeft = 4,
/// <summary>
/// The operator consumes the final top reference.
/// </summary>
TopRight = 8,
/// <summary>
/// The operator consumes the final left reference.
/// </summary>
BottomLeft = 16,
/// <summary>
/// The operator consumes the horizontal smooth weights.
/// </summary>
ColumnWeight = 32,
/// <summary>
/// The operator consumes the vertical smooth weights.
/// </summary>
RowWeight = 64,
}
/// <summary>
/// Defines the scalar and SIMD arithmetic for one non-directional AV1 intra-prediction mode.
/// </summary>
/// <remarks>
/// Each overload performs the same lane-wise operation. The generic predictor traversal selects the widest
/// available overload, and the JIT specializes each static interface call for the closed operator type.
/// </remarks>
internal interface IAv1IntraPredictionOperator
{
/// <summary>
/// Gets the prediction mode implemented by the operator.
/// </summary>
public static abstract Av1PredictionMode Mode { get; }
/// <summary>
/// Gets the neighboring inputs consumed by the operator.
/// </summary>
public static abstract Av1IntraPredictionInputs Inputs { get; }
/// <summary>
/// Predicts one 8-bit sample when hardware vectorization is unavailable.
/// </summary>
/// <param name="top">The top reference sample.</param>
/// <param name="left">The left reference sample.</param>
/// <param name="topLeft">The shared top-left reference sample.</param>
/// <param name="topRight">The final top reference sample.</param>
/// <param name="bottomLeft">The final left reference sample.</param>
/// <param name="columnWeight">The horizontal Q8 smooth weight.</param>
/// <param name="rowWeight">The vertical Q8 smooth weight.</param>
/// <returns>The predicted sample.</returns>
public static abstract byte Predict(byte top, byte left, byte topLeft, byte topRight, byte bottomLeft, int columnWeight, int rowWeight);
/// <summary>
/// Predicts sixteen 8-bit samples in parallel.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference sample in every lane.</param>
/// <param name="topLeft">The shared top-left reference sample in every lane.</param>
/// <param name="topRight">The final top reference sample in every lane.</param>
/// <param name="bottomLeft">The final left reference sample in every lane.</param>
/// <param name="columnWeights">The first horizontal Q8 smooth weight for these lanes.</param>
/// <param name="rowWeight">The vertical Q8 smooth weight.</param>
/// <returns>The predicted samples.</returns>
public static abstract Vector128<byte> Predict(Vector128<byte> top, Vector128<byte> left, Vector128<byte> topLeft, Vector128<byte> topRight, Vector128<byte> bottomLeft, ref int columnWeights, int rowWeight);
/// <summary>
/// Predicts thirty-two 8-bit samples in parallel.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference sample in every lane.</param>
/// <param name="topLeft">The shared top-left reference sample in every lane.</param>
/// <param name="topRight">The final top reference sample in every lane.</param>
/// <param name="bottomLeft">The final left reference sample in every lane.</param>
/// <param name="columnWeights">The first horizontal Q8 smooth weight for these lanes.</param>
/// <param name="rowWeight">The vertical Q8 smooth weight.</param>
/// <returns>The predicted samples.</returns>
public static abstract Vector256<byte> Predict(Vector256<byte> top, Vector256<byte> left, Vector256<byte> topLeft, Vector256<byte> topRight, Vector256<byte> bottomLeft, ref int columnWeights, int rowWeight);
/// <summary>
/// Predicts sixty-four 8-bit samples in parallel.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference sample in every lane.</param>
/// <param name="topLeft">The shared top-left reference sample in every lane.</param>
/// <param name="topRight">The final top reference sample in every lane.</param>
/// <param name="bottomLeft">The final left reference sample in every lane.</param>
/// <param name="columnWeights">The first horizontal Q8 smooth weight for these lanes.</param>
/// <param name="rowWeight">The vertical Q8 smooth weight.</param>
/// <returns>The predicted samples.</returns>
public static abstract Vector512<byte> Predict(Vector512<byte> top, Vector512<byte> left, Vector512<byte> topLeft, Vector512<byte> topRight, Vector512<byte> bottomLeft, ref int columnWeights, int rowWeight);
/// <summary>
/// Predicts one high-bit-depth sample when hardware vectorization is unavailable.
/// </summary>
/// <param name="top">The top reference sample.</param>
/// <param name="left">The left reference sample.</param>
/// <param name="topLeft">The shared top-left reference sample.</param>
/// <param name="topRight">The final top reference sample.</param>
/// <param name="bottomLeft">The final left reference sample.</param>
/// <param name="columnWeight">The horizontal Q8 smooth weight.</param>
/// <param name="rowWeight">The vertical Q8 smooth weight.</param>
/// <returns>The predicted sample.</returns>
public static abstract short Predict(short top, short left, short topLeft, short topRight, short bottomLeft, int columnWeight, int rowWeight);
/// <summary>
/// Predicts eight high-bit-depth samples in parallel.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference sample in every lane.</param>
/// <param name="topLeft">The shared top-left reference sample in every lane.</param>
/// <param name="topRight">The final top reference sample in every lane.</param>
/// <param name="bottomLeft">The final left reference sample in every lane.</param>
/// <param name="columnWeights">The first horizontal Q8 smooth weight for these lanes.</param>
/// <param name="rowWeight">The vertical Q8 smooth weight.</param>
/// <returns>The predicted samples.</returns>
public static abstract Vector128<short> Predict(Vector128<short> top, Vector128<short> left, Vector128<short> topLeft, Vector128<short> topRight, Vector128<short> bottomLeft, ref int columnWeights, int rowWeight);
/// <summary>
/// Predicts sixteen high-bit-depth samples in parallel.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference sample in every lane.</param>
/// <param name="topLeft">The shared top-left reference sample in every lane.</param>
/// <param name="topRight">The final top reference sample in every lane.</param>
/// <param name="bottomLeft">The final left reference sample in every lane.</param>
/// <param name="columnWeights">The first horizontal Q8 smooth weight for these lanes.</param>
/// <param name="rowWeight">The vertical Q8 smooth weight.</param>
/// <returns>The predicted samples.</returns>
public static abstract Vector256<short> Predict(Vector256<short> top, Vector256<short> left, Vector256<short> topLeft, Vector256<short> topRight, Vector256<short> bottomLeft, ref int columnWeights, int rowWeight);
/// <summary>
/// Predicts thirty-two high-bit-depth samples in parallel.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference sample in every lane.</param>
/// <param name="topLeft">The shared top-left reference sample in every lane.</param>
/// <param name="topRight">The final top reference sample in every lane.</param>
/// <param name="bottomLeft">The final left reference sample in every lane.</param>
/// <param name="columnWeights">The first horizontal Q8 smooth weight for these lanes.</param>
/// <param name="rowWeight">The vertical Q8 smooth weight.</param>
/// <returns>The predicted samples.</returns>
public static abstract Vector512<short> Predict(Vector512<short> top, Vector512<short> left, Vector512<short> topLeft, Vector512<short> topRight, Vector512<short> bottomLeft, ref int columnWeights, int rowWeight);
}
/// <summary>
/// Applies one closed non-directional AV1 prediction operator using the widest available SIMD width.
/// </summary>

183
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.OperatorContract.cs

@ -1,183 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// <content>
/// Defines the neighbor-usage flags and scalar/SIMD contract for closed intra-prediction operators.
/// </content>
internal abstract partial class Av1IntraPredictorBase
{
/// <summary>
/// Identifies the neighboring inputs consumed by an AV1 intra-prediction operator.
/// </summary>
[Flags]
internal enum Av1IntraPredictionInputs
{
/// <summary>
/// The operator does not consume neighboring samples.
/// </summary>
None = 0,
/// <summary>
/// The operator consumes samples from the top reference.
/// </summary>
Top = 1,
/// <summary>
/// The operator consumes samples from the left reference.
/// </summary>
Left = 2,
/// <summary>
/// The operator consumes the shared top-left reference.
/// </summary>
TopLeft = 4,
/// <summary>
/// The operator consumes the final top reference.
/// </summary>
TopRight = 8,
/// <summary>
/// The operator consumes the final left reference.
/// </summary>
BottomLeft = 16,
/// <summary>
/// The operator consumes the horizontal smooth weights.
/// </summary>
ColumnWeight = 32,
/// <summary>
/// The operator consumes the vertical smooth weights.
/// </summary>
RowWeight = 64,
}
/// <summary>
/// Defines the scalar and SIMD arithmetic for one non-directional AV1 intra-prediction mode.
/// </summary>
/// <remarks>
/// Each overload performs the same lane-wise operation. The generic predictor traversal selects the widest
/// available overload, and the JIT specializes each static interface call for the closed operator type.
/// </remarks>
internal interface IAv1IntraPredictionOperator
{
/// <summary>
/// Gets the prediction mode implemented by the operator.
/// </summary>
public static abstract Av1PredictionMode Mode { get; }
/// <summary>
/// Gets the neighboring inputs consumed by the operator.
/// </summary>
public static abstract Av1IntraPredictionInputs Inputs { get; }
/// <summary>
/// Predicts one 8-bit sample when hardware vectorization is unavailable.
/// </summary>
/// <param name="top">The top reference sample.</param>
/// <param name="left">The left reference sample.</param>
/// <param name="topLeft">The shared top-left reference sample.</param>
/// <param name="topRight">The final top reference sample.</param>
/// <param name="bottomLeft">The final left reference sample.</param>
/// <param name="columnWeight">The horizontal Q8 smooth weight.</param>
/// <param name="rowWeight">The vertical Q8 smooth weight.</param>
/// <returns>The predicted sample.</returns>
public static abstract byte Predict(byte top, byte left, byte topLeft, byte topRight, byte bottomLeft, int columnWeight, int rowWeight);
/// <summary>
/// Predicts sixteen 8-bit samples in parallel.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference sample in every lane.</param>
/// <param name="topLeft">The shared top-left reference sample in every lane.</param>
/// <param name="topRight">The final top reference sample in every lane.</param>
/// <param name="bottomLeft">The final left reference sample in every lane.</param>
/// <param name="columnWeights">The first horizontal Q8 smooth weight for these lanes.</param>
/// <param name="rowWeight">The vertical Q8 smooth weight.</param>
/// <returns>The predicted samples.</returns>
public static abstract Vector128<byte> Predict(Vector128<byte> top, Vector128<byte> left, Vector128<byte> topLeft, Vector128<byte> topRight, Vector128<byte> bottomLeft, ref int columnWeights, int rowWeight);
/// <summary>
/// Predicts thirty-two 8-bit samples in parallel.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference sample in every lane.</param>
/// <param name="topLeft">The shared top-left reference sample in every lane.</param>
/// <param name="topRight">The final top reference sample in every lane.</param>
/// <param name="bottomLeft">The final left reference sample in every lane.</param>
/// <param name="columnWeights">The first horizontal Q8 smooth weight for these lanes.</param>
/// <param name="rowWeight">The vertical Q8 smooth weight.</param>
/// <returns>The predicted samples.</returns>
public static abstract Vector256<byte> Predict(Vector256<byte> top, Vector256<byte> left, Vector256<byte> topLeft, Vector256<byte> topRight, Vector256<byte> bottomLeft, ref int columnWeights, int rowWeight);
/// <summary>
/// Predicts sixty-four 8-bit samples in parallel.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference sample in every lane.</param>
/// <param name="topLeft">The shared top-left reference sample in every lane.</param>
/// <param name="topRight">The final top reference sample in every lane.</param>
/// <param name="bottomLeft">The final left reference sample in every lane.</param>
/// <param name="columnWeights">The first horizontal Q8 smooth weight for these lanes.</param>
/// <param name="rowWeight">The vertical Q8 smooth weight.</param>
/// <returns>The predicted samples.</returns>
public static abstract Vector512<byte> Predict(Vector512<byte> top, Vector512<byte> left, Vector512<byte> topLeft, Vector512<byte> topRight, Vector512<byte> bottomLeft, ref int columnWeights, int rowWeight);
/// <summary>
/// Predicts one high-bit-depth sample when hardware vectorization is unavailable.
/// </summary>
/// <param name="top">The top reference sample.</param>
/// <param name="left">The left reference sample.</param>
/// <param name="topLeft">The shared top-left reference sample.</param>
/// <param name="topRight">The final top reference sample.</param>
/// <param name="bottomLeft">The final left reference sample.</param>
/// <param name="columnWeight">The horizontal Q8 smooth weight.</param>
/// <param name="rowWeight">The vertical Q8 smooth weight.</param>
/// <returns>The predicted sample.</returns>
public static abstract short Predict(short top, short left, short topLeft, short topRight, short bottomLeft, int columnWeight, int rowWeight);
/// <summary>
/// Predicts eight high-bit-depth samples in parallel.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference sample in every lane.</param>
/// <param name="topLeft">The shared top-left reference sample in every lane.</param>
/// <param name="topRight">The final top reference sample in every lane.</param>
/// <param name="bottomLeft">The final left reference sample in every lane.</param>
/// <param name="columnWeights">The first horizontal Q8 smooth weight for these lanes.</param>
/// <param name="rowWeight">The vertical Q8 smooth weight.</param>
/// <returns>The predicted samples.</returns>
public static abstract Vector128<short> Predict(Vector128<short> top, Vector128<short> left, Vector128<short> topLeft, Vector128<short> topRight, Vector128<short> bottomLeft, ref int columnWeights, int rowWeight);
/// <summary>
/// Predicts sixteen high-bit-depth samples in parallel.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference sample in every lane.</param>
/// <param name="topLeft">The shared top-left reference sample in every lane.</param>
/// <param name="topRight">The final top reference sample in every lane.</param>
/// <param name="bottomLeft">The final left reference sample in every lane.</param>
/// <param name="columnWeights">The first horizontal Q8 smooth weight for these lanes.</param>
/// <param name="rowWeight">The vertical Q8 smooth weight.</param>
/// <returns>The predicted samples.</returns>
public static abstract Vector256<short> Predict(Vector256<short> top, Vector256<short> left, Vector256<short> topLeft, Vector256<short> topRight, Vector256<short> bottomLeft, ref int columnWeights, int rowWeight);
/// <summary>
/// Predicts thirty-two high-bit-depth samples in parallel.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference sample in every lane.</param>
/// <param name="topLeft">The shared top-left reference sample in every lane.</param>
/// <param name="topRight">The final top reference sample in every lane.</param>
/// <param name="bottomLeft">The final left reference sample in every lane.</param>
/// <param name="columnWeights">The first horizontal Q8 smooth weight for these lanes.</param>
/// <param name="rowWeight">The vertical Q8 smooth weight.</param>
/// <returns>The predicted samples.</returns>
public static abstract Vector512<short> Predict(Vector512<short> top, Vector512<short> left, Vector512<short> topLeft, Vector512<short> topRight, Vector512<short> bottomLeft, ref int columnWeights, int rowWeight);
}
}

66
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs

@ -86,7 +86,7 @@ internal class Av1PredictionDecoder
/// <param name="blockModeInfoRowOffset">The transform block's vertical offset within the mode-information block.</param>
/// <remarks>Corresponds to <c>svt_av1_predict_intra</c> in SVT-AV1.</remarks>
public void Decode(
Av1PartitionInfo partitionInfo,
ref Av1PartitionInfo partitionInfo,
Av1Plane plane,
Av1TransformSize transformSize,
Av1TileInfo tileInfo,
@ -96,7 +96,7 @@ internal class Av1PredictionDecoder
int blockModeInfoColumnOffset,
int blockModeInfoRowOffset)
=> this.DecodeCore(
partitionInfo,
ref partitionInfo,
plane,
transformSize,
tileInfo,
@ -120,7 +120,7 @@ internal class Av1PredictionDecoder
/// <param name="blockModeInfoRowOffset">The transform block's vertical offset within the mode-information block.</param>
/// <remarks>Implements the intra prediction portion of section 7.11.2 of the AV1 specification.</remarks>
public void Decode(
Av1PartitionInfo partitionInfo,
ref Av1PartitionInfo partitionInfo,
Av1Plane plane,
Av1TransformSize transformSize,
Av1TileInfo tileInfo,
@ -130,7 +130,7 @@ internal class Av1PredictionDecoder
int blockModeInfoColumnOffset,
int blockModeInfoRowOffset)
=> this.DecodeCore(
partitionInfo,
ref partitionInfo,
plane,
transformSize,
tileInfo,
@ -154,7 +154,7 @@ internal class Av1PredictionDecoder
/// <param name="blockModeInfoColumnOffset">The transform block's horizontal offset within the mode-information block.</param>
/// <param name="blockModeInfoRowOffset">The transform block's vertical offset within the mode-information block.</param>
private void DecodeCore<T>(
Av1PartitionInfo partitionInfo,
ref Av1PartitionInfo partitionInfo,
Av1Plane plane,
Av1TransformSize transformSize,
Av1TileInfo tileInfo,
@ -173,12 +173,11 @@ internal class Av1PredictionDecoder
Span<T> leftNeighbor = pixelBuffer[(stride - 1)..];
Span<T> startOfPixels = pixelBuffer[stride..];
Av1PredictionMode mode = (plane == Av1Plane.Y) ? partitionInfo.ModeInfo.YMode : partitionInfo.ModeInfo.UvMode;
if (plane != Av1Plane.Y && partitionInfo.ModeInfo.UvMode == Av1PredictionMode.UvChromaFromLuma)
Av1PredictionMode mode = partitionInfo.ModeInfo.YMode;
if (plane != Av1Plane.Y && partitionInfo.ModeInfo.UvMode == Av1ChromaPredictionMode.ChromaFromLuma)
{
this.PredictIntraBlock(
partitionInfo,
ref partitionInfo,
plane,
transformSize,
tileInfo,
@ -193,7 +192,7 @@ internal class Av1PredictionDecoder
bitDepth);
this.PredictChromaFromLumaBlock(
partitionInfo,
ref partitionInfo,
partitionInfo.ChromaFromLumaContext,
startOfPixels,
stride,
@ -203,8 +202,15 @@ internal class Av1PredictionDecoder
return;
}
if (plane != Av1Plane.Y)
{
// Chroma and luma modes are separate bitstream domains. Shared spatial predictors consume the explicit
// libaom get_uv_mode() equivalent rather than relying on their matching ordinal values.
mode = partitionInfo.ModeInfo.UvMode.ToLumaMode();
}
this.PredictIntraBlock(
partitionInfo,
ref partitionInfo,
plane,
transformSize,
tileInfo,
@ -229,11 +235,17 @@ internal class Av1PredictionDecoder
/// <param name="stride">The distance, in samples, between pixel rows.</param>
/// <param name="transformSize">The dimensions of the chroma transform block.</param>
/// <param name="plane">The U or V plane being reconstructed.</param>
private void PredictChromaFromLumaBlock<T>(Av1PartitionInfo partitionInfo, Av1ChromaFromLumaContext? chromaFromLumaContext, Span<T> pixelBuffer, int stride, Av1TransformSize transformSize, Av1Plane plane)
private void PredictChromaFromLumaBlock<T>(
ref Av1PartitionInfo partitionInfo,
Av1ChromaFromLumaContext? chromaFromLumaContext,
Span<T> pixelBuffer,
int stride,
Av1TransformSize transformSize,
Av1Plane plane)
where T : unmanaged, IBinaryInteger<T>
{
Av1BlockModeInfo modeInfo = partitionInfo.ModeInfo;
bool isChromaFromLumaAllowedFlag = IsChromaFromLumaAllowedWithFrameHeader(partitionInfo, this.sequenceHeader.ColorConfig, this.frameHeader);
bool isChromaFromLumaAllowedFlag = IsChromaFromLumaAllowedWithFrameHeader(ref partitionInfo, this.sequenceHeader.ColorConfig, this.frameHeader);
DebugGuard.IsTrue(isChromaFromLumaAllowedFlag, "Chroma from Luma should be allowed then computing it.");
if (chromaFromLumaContext == null)
@ -271,7 +283,7 @@ internal class Av1PredictionDecoder
/// <param name="colorConfig">The sequence color configuration.</param>
/// <param name="frameHeader">The decoded frame header.</param>
/// <returns><see langword="true"/> when the block may use chroma-from-luma prediction; otherwise, <see langword="false"/>.</returns>
private static bool IsChromaFromLumaAllowedWithFrameHeader(Av1PartitionInfo partitionInfo, ObuColorConfig colorConfig, ObuFrameHeader frameHeader)
private static bool IsChromaFromLumaAllowedWithFrameHeader(ref Av1PartitionInfo partitionInfo, ObuColorConfig colorConfig, ObuFrameHeader frameHeader)
{
Av1BlockModeInfo modeInfo = partitionInfo.ModeInfo;
Av1BlockSize blockSize = modeInfo.BlockSize;
@ -327,7 +339,7 @@ internal class Av1PredictionDecoder
/// <param name="blockModeInfoRowOffset">The transform block's vertical offset within the mode-information block.</param>
/// <param name="bitDepth">The bit depth of the reconstructed samples.</param>
private void PredictIntraBlock<T>(
Av1PartitionInfo partitionInfo,
ref Av1PartitionInfo partitionInfo,
Av1Plane plane,
Av1TransformSize transformSize,
Av1TileInfo tileInfo,
@ -359,7 +371,7 @@ internal class Av1PredictionDecoder
{
ReadOnlySpan<ushort> paletteColors = modeInfo.GetPaletteColors(plane);
ReadOnlySpan<byte> colorIndexMap = modeInfo.GetPaletteColorIndexMap(plane);
int paletteStride = partitionInfo.WidthInPixels[(int)plane];
int paletteStride = partitionInfo.GetWidthInPixels(plane);
int mapOffset = ((blockModeInfoRowOffset << Av1Constants.ModeInfoSizeLog2) * paletteStride) +
(blockModeInfoColumnOffset << Av1Constants.ModeInfoSizeLog2);
@ -395,11 +407,13 @@ internal class Av1PredictionDecoder
// These distances bound edge extension at the coded frame rather than allowing
// a transform to read padding that happens to exist beyond the visible image.
int xr = (partitionInfo.ModeBlockToRightEdge >> (3 + subX)) + (partitionInfo.WidthInPixels[(int)plane] - (blockModeInfoColumnOffset << Av1Constants.ModeInfoSizeLog2) - transformWidth) -
int xr = (partitionInfo.ModeBlockToRightEdge >> (3 + subX)) +
(partitionInfo.GetWidthInPixels(plane) - (blockModeInfoColumnOffset << Av1Constants.ModeInfoSizeLog2) - transformWidth) -
xrOffset;
int yd = (partitionInfo.ModeBlockToBottomEdge >> (3 + subY)) +
(partitionInfo.HeightInPixels[(int)plane] - (blockModeInfoRowOffset << Av1Constants.ModeInfoSizeLog2) - transformHeight) - ydOffset;
(partitionInfo.GetHeightInPixels(plane) - (blockModeInfoRowOffset << Av1Constants.ModeInfoSizeLog2) - transformHeight) - ydOffset;
bool rightAvailable = modeInfoColumn + ((blockModeInfoColumnOffset + transformWidthInModeInfoUnits) << subX) < tileInfo.ModeInfoColumnEnd;
bool bottomAvailable = (yd > 0) && (modeInfoRow + ((blockModeInfoRowOffset + transformHeightInModeInfoUnits) << subY) < tileInfo.ModeInfoRowEnd);
@ -439,7 +453,7 @@ internal class Av1PredictionDecoder
// Calling all other intra predictors except CFL and palette.
this.DecodeBuildIntraPredictors(
partitionInfo,
ref partitionInfo,
topNeighbor,
leftNeighbor,
(nuint)referenceStride,
@ -752,7 +766,7 @@ internal class Av1PredictionDecoder
/// <param name="plane">The color plane being reconstructed.</param>
/// <param name="bitDepth">The number of bits used to represent each sample.</param>
private void DecodeBuildIntraPredictors<T>(
Av1PartitionInfo partitionInfo,
ref Av1PartitionInfo partitionInfo,
Span<T> aboveNeighbor,
ReadOnlySpan<T> leftNeighbor,
nuint referenceStride,
@ -989,7 +1003,7 @@ internal class Av1PredictionDecoder
bool needRight = angle < 90;
bool needBottom = angle > 180;
bool filterType = GetFilterType(partitionInfo, plane);
bool filterType = GetFilterType(ref partitionInfo, plane);
if (angle is not 90 and not 180)
{
@ -1862,7 +1876,7 @@ internal class Av1PredictionDecoder
/// <param name="partitionInfo">The decoded partition and neighboring mode state.</param>
/// <param name="plane">The color plane whose neighbors are inspected.</param>
/// <returns><see langword="true"/> when either relevant neighbor uses a smooth mode; otherwise, <see langword="false"/>.</returns>
private static bool GetFilterType(Av1PartitionInfo partitionInfo, Av1Plane plane)
private static bool GetFilterType(ref Av1PartitionInfo partitionInfo, Av1Plane plane)
{
Av1BlockModeInfo? above;
Av1BlockModeInfo? left;
@ -1900,10 +1914,10 @@ internal class Av1PredictionDecoder
else
{
// Inter mode not supported here.
Av1PredictionMode uvMode = modeInfo.UvMode;
return uvMode is Av1PredictionMode.Smooth or
Av1PredictionMode.SmoothVertical or
Av1PredictionMode.SmoothHorizontal;
Av1ChromaPredictionMode uvMode = modeInfo.UvMode;
return uvMode is Av1ChromaPredictionMode.Smooth or
Av1ChromaPredictionMode.SmoothVertical or
Av1ChromaPredictionMode.SmoothHorizontal;
}
}
}

100
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionMode.cs

@ -4,10 +4,9 @@
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// <summary>
/// Identifies the intra-prediction modes used by an AV1 still-picture frame.
/// Identifies the luma intra and inter prediction modes used by an AV1 coding block.
/// </summary>
/// <remarks>Inter modes are omitted because reduced still-picture frames do not reference other frames.</remarks>
internal enum Av1PredictionMode
internal enum Av1PredictionMode : byte
{
/// <summary>
/// Predicts each sample from the average of the available top and left neighbors.
@ -75,9 +74,64 @@ internal enum Av1PredictionMode
Paeth,
/// <summary>
/// Predicts chroma from the reconstructed luma AC surface.
/// Uses the nearest motion-vector candidate for one reference frame.
/// </summary>
UvChromaFromLuma,
NearestMotionVector = 13,
/// <summary>
/// Uses a near motion-vector candidate for one reference frame.
/// </summary>
NearMotionVector = 14,
/// <summary>
/// Uses the global-motion model for one reference frame.
/// </summary>
GlobalMotionVector = 15,
/// <summary>
/// Decodes a new motion vector for one reference frame.
/// </summary>
NewMotionVector = 16,
/// <summary>
/// Uses the nearest motion-vector candidate for both compound references.
/// </summary>
NearestNearestMotionVector = 17,
/// <summary>
/// Uses a near motion-vector candidate for both compound references.
/// </summary>
NearNearMotionVector = 18,
/// <summary>
/// Uses the nearest candidate for the first compound reference and decodes a new vector for the second.
/// </summary>
NearestNewMotionVector = 19,
/// <summary>
/// Decodes a new vector for the first compound reference and uses the nearest candidate for the second.
/// </summary>
NewNearestMotionVector = 20,
/// <summary>
/// Uses a near candidate for the first compound reference and decodes a new vector for the second.
/// </summary>
NearNewMotionVector = 21,
/// <summary>
/// Decodes a new vector for the first compound reference and uses a near candidate for the second.
/// </summary>
NewNearMotionVector = 22,
/// <summary>
/// Uses the global-motion model for both compound references.
/// </summary>
GlobalGlobalMotionVector = 23,
/// <summary>
/// Decodes a new motion vector for both compound references.
/// </summary>
NewNewMotionVector = 24,
/// <summary>
/// The first luma intra-prediction mode.
@ -95,12 +149,42 @@ internal enum Av1PredictionMode
IntraModes = Paeth + 1,
/// <summary>
/// The number of chroma intra-prediction modes, including chroma-from-luma.
/// The first single-reference inter-prediction mode.
/// </summary>
SingleInterModeStart = NearestMotionVector,
/// <summary>
/// The exclusive upper bound of single-reference inter-prediction modes.
/// </summary>
SingleInterModeEnd = NearestNearestMotionVector,
/// <summary>
/// The first compound-reference inter-prediction mode.
/// </summary>
CompoundInterModeStart = NearestNearestMotionVector,
/// <summary>
/// The exclusive upper bound of compound-reference inter-prediction modes.
/// </summary>
CompoundInterModeEnd = NewNewMotionVector + 1,
/// <summary>
/// The first inter-prediction mode.
/// </summary>
InterModeStart = NearestMotionVector,
/// <summary>
/// The exclusive upper bound of all inter-prediction modes.
/// </summary>
InterModeEnd = NewNewMotionVector + 1,
/// <summary>
/// The number of luma and inter prediction modes in the complete AV1 mode domain.
/// </summary>
UvIntraModes = UvChromaFromLuma + 1,
PredictionModeCount = NewNewMotionVector + 1,
/// <summary>
/// The invalid intra-mode sentinel matching the complete AV1 prediction-mode domain.
/// </summary>
IntraInvalid = 25,
IntraInvalid = PredictionModeCount,
}

728
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Dispatch.cs

@ -28,8 +28,7 @@ internal static partial class Av1InterPredictor
Av1InterpolationFilter verticalFilter,
int horizontalPhase,
int verticalPhase,
Span<short> scratch,
bool scalarOnly)
Span<short> scratch)
{
switch (horizontalFilter)
{
@ -45,8 +44,7 @@ internal static partial class Av1InterPredictor
verticalFilter,
horizontalPhase,
verticalPhase,
scratch,
scalarOnly);
scratch);
break;
case Av1InterpolationFilter.Smooth:
@ -61,8 +59,7 @@ internal static partial class Av1InterPredictor
verticalFilter,
horizontalPhase,
verticalPhase,
scratch,
scalarOnly);
scratch);
break;
case Av1InterpolationFilter.Sharp:
@ -77,8 +74,7 @@ internal static partial class Av1InterPredictor
verticalFilter,
horizontalPhase,
verticalPhase,
scratch,
scalarOnly);
scratch);
break;
default:
@ -93,8 +89,7 @@ internal static partial class Av1InterPredictor
verticalFilter,
horizontalPhase,
verticalPhase,
scratch,
scalarOnly);
scratch);
break;
}
@ -116,8 +111,7 @@ internal static partial class Av1InterPredictor
int horizontalPhase,
int verticalPhase,
int bitDepth,
Span<short> scratch,
bool scalarOnly)
Span<short> scratch)
{
switch (horizontalFilter)
{
@ -134,8 +128,7 @@ internal static partial class Av1InterPredictor
horizontalPhase,
verticalPhase,
bitDepth,
scratch,
scalarOnly);
scratch);
break;
case Av1InterpolationFilter.Smooth:
@ -151,8 +144,7 @@ internal static partial class Av1InterPredictor
horizontalPhase,
verticalPhase,
bitDepth,
scratch,
scalarOnly);
scratch);
break;
case Av1InterpolationFilter.Sharp:
@ -168,8 +160,7 @@ internal static partial class Av1InterPredictor
horizontalPhase,
verticalPhase,
bitDepth,
scratch,
scalarOnly);
scratch);
break;
default:
@ -185,17 +176,16 @@ internal static partial class Av1InterPredictor
horizontalPhase,
verticalPhase,
bitDepth,
scratch,
scalarOnly);
scratch);
break;
}
}
/// <summary>
/// Copies an 8-bit integer-position block using the widest vector that fits a complete row prefix.
/// Selects a closed 8-bit horizontal interpolation operator for explicit scalar execution.
/// </summary>
private static void Copy(
private static void DispatchScalar(
ReadOnlySpan<byte> source,
int sourceStride,
int sourceOrigin,
@ -203,14 +193,179 @@ internal static partial class Av1InterPredictor
int destinationStride,
int width,
int height,
bool scalarOnly)
Av1InterpolationFilter horizontalFilter,
Av1InterpolationFilter verticalFilter,
int horizontalPhase,
int verticalPhase,
Span<short> scratch)
{
if (scalarOnly)
// The benchmark/test entry point closes the same production operators explicitly, but terminates in the
// scalar kernels without carrying a runtime mode flag through the SIMD-first decoder path.
switch (horizontalFilter)
{
CopyScalar(source, sourceStride, sourceOrigin, destination, destinationStride, width, height);
return;
case Av1InterpolationFilter.Regular:
DispatchVerticalScalar<RegularOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
verticalFilter,
horizontalPhase,
verticalPhase,
scratch);
break;
case Av1InterpolationFilter.Smooth:
DispatchVerticalScalar<SmoothOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
verticalFilter,
horizontalPhase,
verticalPhase,
scratch);
break;
case Av1InterpolationFilter.Sharp:
DispatchVerticalScalar<SharpOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
verticalFilter,
horizontalPhase,
verticalPhase,
scratch);
break;
default:
DispatchVerticalScalar<BilinearOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
verticalFilter,
horizontalPhase,
verticalPhase,
scratch);
break;
}
}
/// <summary>
/// Selects a closed high-bit-depth horizontal interpolation operator for explicit scalar execution.
/// </summary>
private static void DispatchScalar(
ReadOnlySpan<ushort> source,
int sourceStride,
int sourceOrigin,
Span<ushort> destination,
int destinationStride,
int width,
int height,
Av1InterpolationFilter horizontalFilter,
Av1InterpolationFilter verticalFilter,
int horizontalPhase,
int verticalPhase,
int bitDepth,
Span<short> scratch)
{
// Closing the production table operators here keeps scalar parity coverage on the same normative Q7 data.
switch (horizontalFilter)
{
case Av1InterpolationFilter.Regular:
DispatchVerticalScalar<RegularOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
verticalFilter,
horizontalPhase,
verticalPhase,
bitDepth,
scratch);
break;
case Av1InterpolationFilter.Smooth:
DispatchVerticalScalar<SmoothOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
verticalFilter,
horizontalPhase,
verticalPhase,
bitDepth,
scratch);
break;
case Av1InterpolationFilter.Sharp:
DispatchVerticalScalar<SharpOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
verticalFilter,
horizontalPhase,
verticalPhase,
bitDepth,
scratch);
break;
default:
DispatchVerticalScalar<BilinearOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
verticalFilter,
horizontalPhase,
verticalPhase,
bitDepth,
scratch);
break;
}
}
/// <summary>
/// Copies an 8-bit integer-position block using the widest vector that fits a complete row prefix.
/// </summary>
private static void Copy(
ReadOnlySpan<byte> source,
int sourceStride,
int sourceOrigin,
Span<byte> destination,
int destinationStride,
int width,
int height)
{
ref byte sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin);
ref byte destinationBase = ref MemoryMarshal.GetReference(destination);
@ -302,15 +457,8 @@ internal static partial class Av1InterPredictor
Span<ushort> destination,
int destinationStride,
int width,
int height,
bool scalarOnly)
int height)
{
if (scalarOnly)
{
CopyScalar(source, sourceStride, sourceOrigin, destination, destinationStride, width, height);
return;
}
ref ushort sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
@ -409,73 +557,69 @@ internal static partial class Av1InterPredictor
int sourceOffset,
int tapStride,
int firstRound,
int secondRound,
bool scalarOnly)
int secondRound)
{
if (!scalarOnly)
if (Vector512.IsHardwareAccelerated && Vector<int>.Count == Vector512<int>.Count && width >= Vector512<byte>.Count)
{
if (Vector512.IsHardwareAccelerated && Vector<int>.Count == Vector512<int>.Count && width >= Vector512<byte>.Count)
{
FilterDirect(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
coefficients,
tapCount,
sourceOffset,
tapStride,
firstRound,
secondRound,
Vector512<int>.Zero);
return;
}
FilterDirect(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
coefficients,
tapCount,
sourceOffset,
tapStride,
firstRound,
secondRound,
Vector512<int>.Zero);
if (Vector256.IsHardwareAccelerated && width >= Vector256<byte>.Count)
{
FilterDirect(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
coefficients,
tapCount,
sourceOffset,
tapStride,
firstRound,
secondRound,
Vector256<int>.Zero);
return;
}
return;
}
if (Vector128.IsHardwareAccelerated)
{
FilterDirect(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
coefficients,
tapCount,
sourceOffset,
tapStride,
firstRound,
secondRound,
Vector128<int>.Zero);
return;
}
if (Vector256.IsHardwareAccelerated && width >= Vector256<byte>.Count)
{
FilterDirect(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
coefficients,
tapCount,
sourceOffset,
tapStride,
firstRound,
secondRound,
Vector256<int>.Zero);
return;
}
if (Vector128.IsHardwareAccelerated)
{
FilterDirect(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
coefficients,
tapCount,
sourceOffset,
tapStride,
firstRound,
secondRound,
Vector128<int>.Zero);
return;
}
FilterDirectScalar(
@ -511,76 +655,72 @@ internal static partial class Av1InterPredictor
int tapStride,
int firstRound,
int secondRound,
int bitDepth,
bool scalarOnly)
int bitDepth)
{
if (!scalarOnly)
if (Vector512.IsHardwareAccelerated && Vector<int>.Count == Vector512<int>.Count && width >= Vector512<ushort>.Count)
{
if (Vector512.IsHardwareAccelerated && Vector<int>.Count == Vector512<int>.Count && width >= Vector512<ushort>.Count)
{
FilterDirect(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
coefficients,
tapCount,
sourceOffset,
tapStride,
firstRound,
secondRound,
bitDepth,
Vector512<int>.Zero);
FilterDirect(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
coefficients,
tapCount,
sourceOffset,
tapStride,
firstRound,
secondRound,
bitDepth,
Vector512<int>.Zero);
return;
}
return;
}
if (Vector256.IsHardwareAccelerated && width >= Vector256<ushort>.Count)
{
FilterDirect(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
coefficients,
tapCount,
sourceOffset,
tapStride,
firstRound,
secondRound,
bitDepth,
Vector256<int>.Zero);
if (Vector256.IsHardwareAccelerated && width >= Vector256<ushort>.Count)
{
FilterDirect(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
coefficients,
tapCount,
sourceOffset,
tapStride,
firstRound,
secondRound,
bitDepth,
Vector256<int>.Zero);
return;
}
return;
}
if (Vector128.IsHardwareAccelerated)
{
FilterDirect(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
coefficients,
tapCount,
sourceOffset,
tapStride,
firstRound,
secondRound,
bitDepth,
Vector128<int>.Zero);
if (Vector128.IsHardwareAccelerated)
{
FilterDirect(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
coefficients,
tapCount,
sourceOffset,
tapStride,
firstRound,
secondRound,
bitDepth,
Vector128<int>.Zero);
return;
}
return;
}
FilterDirectScalar(
@ -618,82 +758,78 @@ internal static partial class Av1InterPredictor
int verticalTapCount,
int verticalSourceOffset,
int bitDepth,
Span<short> scratch,
bool scalarOnly)
Span<short> scratch)
{
if (!scalarOnly)
if (Vector512.IsHardwareAccelerated && Vector<int>.Count == Vector512<int>.Count && width >= Vector512<byte>.Count)
{
if (Vector512.IsHardwareAccelerated && Vector<int>.Count == Vector512<int>.Count && width >= Vector512<byte>.Count)
{
Filter2D(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalCoefficients,
horizontalTapCount,
horizontalSourceOffset,
verticalCoefficients,
verticalTapCount,
verticalSourceOffset,
bitDepth,
Round0Bits,
scratch,
Vector512<int>.Zero);
Filter2D(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalCoefficients,
horizontalTapCount,
horizontalSourceOffset,
verticalCoefficients,
verticalTapCount,
verticalSourceOffset,
bitDepth,
Round0Bits,
scratch,
Vector512<int>.Zero);
return;
}
return;
}
if (Vector256.IsHardwareAccelerated && width >= Vector256<byte>.Count)
{
Filter2D(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalCoefficients,
horizontalTapCount,
horizontalSourceOffset,
verticalCoefficients,
verticalTapCount,
verticalSourceOffset,
bitDepth,
Round0Bits,
scratch,
Vector256<int>.Zero);
if (Vector256.IsHardwareAccelerated && width >= Vector256<byte>.Count)
{
Filter2D(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalCoefficients,
horizontalTapCount,
horizontalSourceOffset,
verticalCoefficients,
verticalTapCount,
verticalSourceOffset,
bitDepth,
Round0Bits,
scratch,
Vector256<int>.Zero);
return;
}
return;
}
if (Vector128.IsHardwareAccelerated)
{
Filter2D(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalCoefficients,
horizontalTapCount,
horizontalSourceOffset,
verticalCoefficients,
verticalTapCount,
verticalSourceOffset,
bitDepth,
Round0Bits,
scratch,
Vector128<int>.Zero);
if (Vector128.IsHardwareAccelerated)
{
Filter2D(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalCoefficients,
horizontalTapCount,
horizontalSourceOffset,
verticalCoefficients,
verticalTapCount,
verticalSourceOffset,
bitDepth,
Round0Bits,
scratch,
Vector128<int>.Zero);
return;
}
return;
}
Filter2DScalar(
@ -734,82 +870,78 @@ internal static partial class Av1InterPredictor
int verticalSourceOffset,
int bitDepth,
int round0,
Span<short> scratch,
bool scalarOnly)
Span<short> scratch)
{
if (!scalarOnly)
if (Vector512.IsHardwareAccelerated && Vector<int>.Count == Vector512<int>.Count && width >= Vector512<ushort>.Count)
{
if (Vector512.IsHardwareAccelerated && Vector<int>.Count == Vector512<int>.Count && width >= Vector512<ushort>.Count)
{
Filter2D(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalCoefficients,
horizontalTapCount,
horizontalSourceOffset,
verticalCoefficients,
verticalTapCount,
verticalSourceOffset,
bitDepth,
round0,
scratch,
Vector512<int>.Zero);
Filter2D(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalCoefficients,
horizontalTapCount,
horizontalSourceOffset,
verticalCoefficients,
verticalTapCount,
verticalSourceOffset,
bitDepth,
round0,
scratch,
Vector512<int>.Zero);
return;
}
return;
}
if (Vector256.IsHardwareAccelerated && width >= Vector256<ushort>.Count)
{
Filter2D(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalCoefficients,
horizontalTapCount,
horizontalSourceOffset,
verticalCoefficients,
verticalTapCount,
verticalSourceOffset,
bitDepth,
round0,
scratch,
Vector256<int>.Zero);
if (Vector256.IsHardwareAccelerated && width >= Vector256<ushort>.Count)
{
Filter2D(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalCoefficients,
horizontalTapCount,
horizontalSourceOffset,
verticalCoefficients,
verticalTapCount,
verticalSourceOffset,
bitDepth,
round0,
scratch,
Vector256<int>.Zero);
return;
}
return;
}
if (Vector128.IsHardwareAccelerated)
{
Filter2D(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalCoefficients,
horizontalTapCount,
horizontalSourceOffset,
verticalCoefficients,
verticalTapCount,
verticalSourceOffset,
bitDepth,
round0,
scratch,
Vector128<int>.Zero);
if (Vector128.IsHardwareAccelerated)
{
Filter2D(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalCoefficients,
horizontalTapCount,
horizontalSourceOffset,
verticalCoefficients,
verticalTapCount,
verticalSourceOffset,
bitDepth,
round0,
scratch,
Vector128<int>.Zero);
return;
}
return;
}
Filter2DScalar(

419
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Operator.cs

@ -41,8 +41,7 @@ internal static partial class Av1InterPredictor
Av1InterpolationFilter verticalFilter,
int horizontalPhase,
int verticalPhase,
Span<short> scratch,
bool scalarOnly)
Span<short> scratch)
where THorizontal : struct, IAv1InterPredictorOperator
{
switch (verticalFilter)
@ -58,8 +57,7 @@ internal static partial class Av1InterPredictor
height,
horizontalPhase,
verticalPhase,
scratch,
scalarOnly);
scratch);
break;
case Av1InterpolationFilter.Smooth:
@ -73,8 +71,7 @@ internal static partial class Av1InterPredictor
height,
horizontalPhase,
verticalPhase,
scratch,
scalarOnly);
scratch);
break;
case Av1InterpolationFilter.Sharp:
@ -88,8 +85,7 @@ internal static partial class Av1InterPredictor
height,
horizontalPhase,
verticalPhase,
scratch,
scalarOnly);
scratch);
break;
default:
@ -103,8 +99,7 @@ internal static partial class Av1InterPredictor
height,
horizontalPhase,
verticalPhase,
scratch,
scalarOnly);
scratch);
break;
}
@ -126,8 +121,7 @@ internal static partial class Av1InterPredictor
int horizontalPhase,
int verticalPhase,
int bitDepth,
Span<short> scratch,
bool scalarOnly)
Span<short> scratch)
where THorizontal : struct, IAv1InterPredictorOperator
{
switch (verticalFilter)
@ -144,8 +138,7 @@ internal static partial class Av1InterPredictor
horizontalPhase,
verticalPhase,
bitDepth,
scratch,
scalarOnly);
scratch);
break;
case Av1InterpolationFilter.Smooth:
@ -160,8 +153,7 @@ internal static partial class Av1InterPredictor
horizontalPhase,
verticalPhase,
bitDepth,
scratch,
scalarOnly);
scratch);
break;
case Av1InterpolationFilter.Sharp:
@ -176,8 +168,7 @@ internal static partial class Av1InterPredictor
horizontalPhase,
verticalPhase,
bitDepth,
scratch,
scalarOnly);
scratch);
break;
default:
@ -192,8 +183,170 @@ internal static partial class Av1InterPredictor
horizontalPhase,
verticalPhase,
bitDepth,
scratch,
scalarOnly);
scratch);
break;
}
}
/// <summary>
/// Selects an 8-bit vertical interpolation operator for explicit scalar execution.
/// </summary>
/// <typeparam name="THorizontal">The closed horizontal filter family selected from the production operator set.</typeparam>
private static void DispatchVerticalScalar<THorizontal>(
ReadOnlySpan<byte> source,
int sourceStride,
int sourceOrigin,
Span<byte> destination,
int destinationStride,
int width,
int height,
Av1InterpolationFilter verticalFilter,
int horizontalPhase,
int verticalPhase,
Span<short> scratch)
where THorizontal : struct, IAv1InterPredictorOperator
{
switch (verticalFilter)
{
case Av1InterpolationFilter.Regular:
PredictScalar<THorizontal, RegularOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalPhase,
verticalPhase,
scratch);
break;
case Av1InterpolationFilter.Smooth:
PredictScalar<THorizontal, SmoothOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalPhase,
verticalPhase,
scratch);
break;
case Av1InterpolationFilter.Sharp:
PredictScalar<THorizontal, SharpOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalPhase,
verticalPhase,
scratch);
break;
default:
PredictScalar<THorizontal, BilinearOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalPhase,
verticalPhase,
scratch);
break;
}
}
/// <summary>
/// Selects a high-bit-depth vertical interpolation operator for explicit scalar execution.
/// </summary>
/// <typeparam name="THorizontal">The closed horizontal filter family selected from the production operator set.</typeparam>
private static void DispatchVerticalScalar<THorizontal>(
ReadOnlySpan<ushort> source,
int sourceStride,
int sourceOrigin,
Span<ushort> destination,
int destinationStride,
int width,
int height,
Av1InterpolationFilter verticalFilter,
int horizontalPhase,
int verticalPhase,
int bitDepth,
Span<short> scratch)
where THorizontal : struct, IAv1InterPredictorOperator
{
switch (verticalFilter)
{
case Av1InterpolationFilter.Regular:
PredictScalar<THorizontal, RegularOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalPhase,
verticalPhase,
bitDepth,
scratch);
break;
case Av1InterpolationFilter.Smooth:
PredictScalar<THorizontal, SmoothOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalPhase,
verticalPhase,
bitDepth,
scratch);
break;
case Av1InterpolationFilter.Sharp:
PredictScalar<THorizontal, SharpOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalPhase,
verticalPhase,
bitDepth,
scratch);
break;
default:
PredictScalar<THorizontal, BilinearOperator>(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalPhase,
verticalPhase,
bitDepth,
scratch);
break;
}
@ -214,14 +367,13 @@ internal static partial class Av1InterPredictor
int height,
int horizontalPhase,
int verticalPhase,
Span<short> scratch,
bool scalarOnly)
Span<short> scratch)
where THorizontal : struct, IAv1InterPredictorOperator
where TVertical : struct, IAv1InterPredictorOperator
{
if (horizontalPhase == 0 && verticalPhase == 0)
{
Copy(source, sourceStride, sourceOrigin, destination, destinationStride, width, height, scalarOnly);
Copy(source, sourceStride, sourceOrigin, destination, destinationStride, width, height);
return;
}
@ -243,8 +395,7 @@ internal static partial class Av1InterPredictor
firstCoefficient - 3,
1,
Round0Bits,
FilterBits - Round0Bits,
scalarOnly);
FilterBits - Round0Bits);
return;
}
@ -267,8 +418,7 @@ internal static partial class Av1InterPredictor
(firstCoefficient - 3) * sourceStride,
sourceStride,
FilterBits,
0,
scalarOnly);
0);
return;
}
@ -293,8 +443,7 @@ internal static partial class Av1InterPredictor
verticalTapCount,
firstVerticalCoefficient - 3,
8,
scratch,
scalarOnly);
scratch);
}
/// <summary>
@ -313,14 +462,13 @@ internal static partial class Av1InterPredictor
int horizontalPhase,
int verticalPhase,
int bitDepth,
Span<short> scratch,
bool scalarOnly)
Span<short> scratch)
where THorizontal : struct, IAv1InterPredictorOperator
where TVertical : struct, IAv1InterPredictorOperator
{
if (horizontalPhase == 0 && verticalPhase == 0)
{
Copy(source, sourceStride, sourceOrigin, destination, destinationStride, width, height, scalarOnly);
Copy(source, sourceStride, sourceOrigin, destination, destinationStride, width, height);
return;
}
@ -348,8 +496,7 @@ internal static partial class Av1InterPredictor
1,
round0,
FilterBits - round0,
bitDepth,
scalarOnly);
bitDepth);
return;
}
@ -373,8 +520,7 @@ internal static partial class Av1InterPredictor
sourceStride,
FilterBits,
0,
bitDepth,
scalarOnly);
bitDepth);
return;
}
@ -400,8 +546,205 @@ internal static partial class Av1InterPredictor
firstVerticalCoefficient - 3,
bitDepth,
round0,
scratch,
scalarOnly);
scratch);
}
/// <summary>
/// Executes one closed 8-bit interpolation-filter pair through the explicit scalar kernels.
/// </summary>
/// <typeparam name="THorizontal">The horizontal filter family.</typeparam>
/// <typeparam name="TVertical">The vertical filter family.</typeparam>
private static void PredictScalar<THorizontal, TVertical>(
ReadOnlySpan<byte> source,
int sourceStride,
int sourceOrigin,
Span<byte> destination,
int destinationStride,
int width,
int height,
int horizontalPhase,
int verticalPhase,
Span<short> scratch)
where THorizontal : struct, IAv1InterPredictorOperator
where TVertical : struct, IAv1InterPredictorOperator
{
if (horizontalPhase == 0 && verticalPhase == 0)
{
CopyScalar(source, sourceStride, sourceOrigin, destination, destinationStride, width, height);
return;
}
if (verticalPhase == 0)
{
ReadOnlySpan<short> coefficients = THorizontal.GetCoefficients(horizontalPhase, width <= 4);
GetEffectiveKernel(coefficients, out int firstCoefficient, out int tapCount);
FilterDirectScalar(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
coefficients[firstCoefficient..],
tapCount,
firstCoefficient - 3,
1,
Round0Bits,
FilterBits - Round0Bits);
return;
}
if (horizontalPhase == 0)
{
ReadOnlySpan<short> coefficients = TVertical.GetCoefficients(verticalPhase, height <= 4);
GetEffectiveKernel(coefficients, out int firstCoefficient, out int tapCount);
FilterDirectScalar(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
coefficients[firstCoefficient..],
tapCount,
(firstCoefficient - 3) * sourceStride,
sourceStride,
FilterBits,
0);
return;
}
ReadOnlySpan<short> horizontalCoefficients = THorizontal.GetCoefficients(horizontalPhase, width <= 4);
ReadOnlySpan<short> verticalCoefficients = TVertical.GetCoefficients(verticalPhase, height <= 4);
GetEffectiveKernel(horizontalCoefficients, out int firstHorizontalCoefficient, out int horizontalTapCount);
GetEffectiveKernel(verticalCoefficients, out int firstVerticalCoefficient, out int verticalTapCount);
Filter2DScalar(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalCoefficients[firstHorizontalCoefficient..],
horizontalTapCount,
firstHorizontalCoefficient - 3,
verticalCoefficients[firstVerticalCoefficient..],
verticalTapCount,
firstVerticalCoefficient - 3,
8,
Round0Bits,
scratch);
}
/// <summary>
/// Executes one closed high-bit-depth interpolation-filter pair through the explicit scalar kernels.
/// </summary>
/// <typeparam name="THorizontal">The horizontal filter family.</typeparam>
/// <typeparam name="TVertical">The vertical filter family.</typeparam>
private static void PredictScalar<THorizontal, TVertical>(
ReadOnlySpan<ushort> source,
int sourceStride,
int sourceOrigin,
Span<ushort> destination,
int destinationStride,
int width,
int height,
int horizontalPhase,
int verticalPhase,
int bitDepth,
Span<short> scratch)
where THorizontal : struct, IAv1InterPredictorOperator
where TVertical : struct, IAv1InterPredictorOperator
{
if (horizontalPhase == 0 && verticalPhase == 0)
{
CopyScalar(source, sourceStride, sourceOrigin, destination, destinationStride, width, height);
return;
}
// Scalar parity uses the same first-pass range correction as the SIMD traversal so twelve-bit
// intermediates remain signed-16-bit without changing the complete Q14 rounding distance.
int intermediateRange = bitDepth + FilterBits - Round0Bits + 2;
int round0 = Round0Bits + Math.Max(intermediateRange - 16, 0);
if (verticalPhase == 0)
{
ReadOnlySpan<short> coefficients = THorizontal.GetCoefficients(horizontalPhase, width <= 4);
GetEffectiveKernel(coefficients, out int firstCoefficient, out int tapCount);
FilterDirectScalar(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
coefficients[firstCoefficient..],
tapCount,
firstCoefficient - 3,
1,
round0,
FilterBits - round0,
bitDepth);
return;
}
if (horizontalPhase == 0)
{
ReadOnlySpan<short> coefficients = TVertical.GetCoefficients(verticalPhase, height <= 4);
GetEffectiveKernel(coefficients, out int firstCoefficient, out int tapCount);
FilterDirectScalar(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
coefficients[firstCoefficient..],
tapCount,
(firstCoefficient - 3) * sourceStride,
sourceStride,
FilterBits,
0,
bitDepth);
return;
}
ReadOnlySpan<short> horizontalCoefficients = THorizontal.GetCoefficients(horizontalPhase, width <= 4);
ReadOnlySpan<short> verticalCoefficients = TVertical.GetCoefficients(verticalPhase, height <= 4);
GetEffectiveKernel(horizontalCoefficients, out int firstHorizontalCoefficient, out int horizontalTapCount);
GetEffectiveKernel(verticalCoefficients, out int firstVerticalCoefficient, out int verticalTapCount);
Filter2DScalar(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalCoefficients[firstHorizontalCoefficient..],
horizontalTapCount,
firstHorizontalCoefficient - 3,
verticalCoefficients[firstVerticalCoefficient..],
verticalTapCount,
firstVerticalCoefficient - 3,
bitDepth,
round0,
scratch);
}
/// <summary>

16
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.cs

@ -92,8 +92,7 @@ internal static partial class Av1InterPredictor
verticalFilter,
horizontalPhase,
verticalPhase,
scratch,
false);
scratch);
/// <summary>
/// Reconstructs an 8-, 10-, or 12-bit translational prediction using the widest supported SIMD kernel.
@ -140,8 +139,7 @@ internal static partial class Av1InterPredictor
horizontalPhase,
verticalPhase,
bitDepth,
scratch,
false);
scratch);
/// <summary>
/// Reconstructs an 8-bit translational prediction without explicit hardware intrinsics.
@ -173,7 +171,7 @@ internal static partial class Av1InterPredictor
int horizontalPhase,
int verticalPhase,
Span<short> scratch)
=> Dispatch(
=> DispatchScalar(
source,
sourceStride,
sourceOrigin,
@ -185,8 +183,7 @@ internal static partial class Av1InterPredictor
verticalFilter,
horizontalPhase,
verticalPhase,
scratch,
true);
scratch);
/// <summary>
/// Reconstructs an 8-, 10-, or 12-bit translational prediction without explicit hardware intrinsics.
@ -220,7 +217,7 @@ internal static partial class Av1InterPredictor
int verticalPhase,
int bitDepth,
Span<short> scratch)
=> Dispatch(
=> DispatchScalar(
source,
sourceStride,
sourceOrigin,
@ -233,6 +230,5 @@ internal static partial class Av1InterPredictor
horizontalPhase,
verticalPhase,
bitDepth,
scratch,
true);
scratch);
}

17
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterpolationFilter.cs

@ -4,27 +4,32 @@
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
/// <summary>
/// Identifies an AV1 interpolation filter used for translational inter prediction.
/// Identifies an AV1 interpolation-filter family or the frame-level switchable selection.
/// </summary>
internal enum Av1InterpolationFilter
internal enum Av1InterpolationFilter : byte
{
/// <summary>
/// The regular interpolation-filter family.
/// </summary>
Regular,
Regular = 0,
/// <summary>
/// The smooth interpolation-filter family.
/// </summary>
Smooth,
Smooth = 1,
/// <summary>
/// The sharp interpolation-filter family.
/// </summary>
Sharp,
Sharp = 2,
/// <summary>
/// The bilinear interpolation-filter family.
/// </summary>
Bilinear,
Bilinear = 3,
/// <summary>
/// Indicates that each inter block selects its interpolation-filter family.
/// </summary>
Switchable = 4,
}

137
src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.Operators.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.BilinearOperator.cs

@ -5,143 +5,8 @@ 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>
@ -154,7 +19,7 @@ internal static partial class Av1IntraBlockCopyPredictor
/// 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
private readonly struct BilinearOperator : IAv1IntraBlockCopyOperator
{
/// <inheritdoc/>
public static bool UsesRight => true;

78
src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.HorizontalOperator.cs

@ -0,0 +1,78 @@
// 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 : IAv1IntraBlockCopyOperator
{
/// <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);
}
}

135
src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.Operator.cs

@ -8,10 +8,135 @@ 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.
/// Defines the scalar and SIMD contract for closed intra-block-copy filter operators, and provides their shared
/// width-progressive SIMD traversal.
/// </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 IAv1IntraBlockCopyOperator
{
/// <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);
}
/// <summary>
/// Applies one closed interpolation operator to an 8-bit source block.
/// </summary>
@ -23,7 +148,7 @@ internal static partial class Av1IntraBlockCopyPredictor
int destinationStride,
int width,
int height)
where TOperator : struct, IOperator
where TOperator : struct, IAv1IntraBlockCopyOperator
{
ref byte sourceBase = ref MemoryMarshal.GetReference(source);
ref byte destinationBase = ref MemoryMarshal.GetReference(destination);
@ -181,7 +306,7 @@ internal static partial class Av1IntraBlockCopyPredictor
int destinationStride,
int width,
int height)
where TOperator : struct, IOperator
where TOperator : struct, IAv1IntraBlockCopyOperator
{
ref short sourceBase = ref MemoryMarshal.GetReference(source);
ref short destinationBase = ref MemoryMarshal.GetReference(destination);
@ -329,7 +454,7 @@ internal static partial class Av1IntraBlockCopyPredictor
int destinationStride,
int width,
int height)
where TOperator : struct, IOperator
where TOperator : struct, IAv1IntraBlockCopyOperator
{
for (int row = 0; row < height; row++)
{
@ -358,7 +483,7 @@ internal static partial class Av1IntraBlockCopyPredictor
int destinationStride,
int width,
int height)
where TOperator : struct, IOperator
where TOperator : struct, IAv1IntraBlockCopyOperator
{
for (int row = 0; row < height; row++)
{

136
src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.OperatorContract.cs

@ -1,136 +0,0 @@
// 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);
}
}

75
src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.VerticalOperator.cs

@ -0,0 +1,75 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy;
internal static partial class Av1IntraBlockCopyPredictor
{
/// <summary>
/// Averages vertically adjacent source samples for a half-sample vertical phase.
/// </summary>
private readonly struct VerticalOperator : IAv1IntraBlockCopyOperator
{
/// <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);
}
}

135
src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrame.cs

@ -0,0 +1,135 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
/// <summary>
/// Owns the completed decoded state retained for one AV1 reference or presentation frame.
/// </summary>
/// <remarks>
/// Reference-map owners contain reconstruction samples after the normative in-loop filters and before film-grain
/// synthesis. A presentation-only owner may instead contain the independently synthesized grained output.
/// </remarks>
internal sealed class Av1ReferenceFrame : IDisposable
{
/// <summary>
/// The completed sample planes while this instance owns them.
/// </summary>
private Av1FrameBuffer<byte>? frameBuffer;
/// <summary>
/// The independently retained entropy snapshot while this frame owner remains alive.
/// </summary>
private Av1FrameEntropyContext? entropyContext;
/// <summary>
/// The decoder-session owner that receives <see cref="entropyContext"/> when this frame is released.
/// </summary>
private Av1FrameEntropyContexts? entropyContextOwner;
/// <summary>
/// Initializes a new instance of the <see cref="Av1ReferenceFrame"/> class and takes ownership of the decoded
/// sample buffer.
/// </summary>
/// <param name="frameBuffer">
/// The completed sample buffer. Ownership transfers to this instance when construction succeeds.
/// </param>
/// <param name="frameHeader">
/// The completed frame header associated with the reconstructed samples. The caller must not mutate the header
/// after transferring it to this instance.
/// </param>
/// <param name="frameInfo">
/// The completed per-block state associated with the reconstructed samples. The caller must not mutate the state
/// after transferring it to this instance.
/// </param>
public Av1ReferenceFrame(Av1FrameBuffer<byte> frameBuffer, ObuFrameHeader frameHeader, Av1FrameInfo frameInfo)
{
this.frameBuffer = frameBuffer;
this.FrameHeader = frameHeader;
this.FrameInfo = frameInfo;
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1ReferenceFrame"/> class and takes ownership of decoded samples
/// and the entropy snapshot retained by a refreshed reference frame.
/// </summary>
/// <param name="frameBuffer">
/// The completed sample buffer. Ownership transfers to this instance when construction succeeds.
/// </param>
/// <param name="frameHeader">
/// The completed frame header associated with the reconstructed samples. The caller must not mutate the header
/// after transferring it to this instance.
/// </param>
/// <param name="frameInfo">
/// The completed per-block state associated with the reconstructed samples. The caller must not mutate the state
/// after transferring it to this instance.
/// </param>
/// <param name="entropyContext">The completed entropy snapshot selected for later primary-reference use.</param>
/// <param name="entropyContextOwner">The decoder-session owner to which the snapshot is returned.</param>
public Av1ReferenceFrame(
Av1FrameBuffer<byte> frameBuffer,
ObuFrameHeader frameHeader,
Av1FrameInfo frameInfo,
Av1FrameEntropyContext entropyContext,
Av1FrameEntropyContexts entropyContextOwner)
: this(frameBuffer, frameHeader, frameInfo)
{
this.entropyContext = entropyContext;
this.entropyContextOwner = entropyContextOwner;
}
/// <summary>
/// Gets the completed sample buffer owned by this frame.
/// </summary>
public Av1FrameBuffer<byte> FrameBuffer => this.frameBuffer!;
/// <summary>
/// Gets the completed header that describes the retained frame.
/// </summary>
public ObuFrameHeader FrameHeader { get; }
/// <summary>
/// Gets the decoded per-block mode, motion, transform, and filter state associated with the retained frame.
/// </summary>
public Av1FrameInfo FrameInfo { get; }
/// <summary>
/// Gets the entropy context retained for primary-reference use, or <see langword="null"/> for a presentation-only
/// frame.
/// </summary>
public Av1FrameEntropyContext? EntropyContext => this.entropyContext;
/// <summary>
/// Transfers the completed sample planes out of this frame owner.
/// </summary>
/// <returns>The completed sample planes now owned by the caller.</returns>
public Av1FrameBuffer<byte> TakeFrameBuffer()
{
Av1FrameBuffer<byte> result = this.frameBuffer!;
this.frameBuffer = null;
return result;
}
/// <summary>
/// Releases the owned completed sample planes and returns any retained entropy snapshot to its decoder session.
/// </summary>
public void Dispose()
{
Av1FrameEntropyContext? context = this.entropyContext;
this.entropyContext = null;
if (context is not null)
{
// Nulling the field before returning the graph makes repeated disposal harmless and guarantees that one
// shared frame owner occupying multiple reference slots returns its snapshot exactly once.
this.entropyContextOwner!.ReturnSnapshot(context);
this.entropyContextOwner = null;
}
this.frameBuffer?.Dispose();
this.frameBuffer = null;
}
}

111
src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameBorder.cs

@ -0,0 +1,111 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
/// <summary>
/// Extends reconstructed AV1 edge samples through the padded reference-frame border.
/// </summary>
internal static class Av1ReferenceFrameBorder
{
/// <summary>
/// Replicates every visible plane edge through its complete decoder padding.
/// </summary>
/// <param name="frameBuffer">The post-restoration reference frame whose padding is extended.</param>
public static void Extend(Av1FrameBuffer<byte> frameBuffer)
{
ObuColorConfig colorConfig = frameBuffer.ColorConfig;
int subsamplingX = !colorConfig.IsMonochrome && colorConfig.SubSamplingX ? 1 : 0;
int subsamplingY = !colorConfig.IsMonochrome && colorConfig.SubSamplingY ? 1 : 0;
ExtendPlane(
frameBuffer,
frameBuffer.BufferY!,
frameBuffer.OriginX,
frameBuffer.OriginY,
frameBuffer.Width,
frameBuffer.Height);
if (!colorConfig.IsMonochrome)
{
int chromaWidth = Av1Math.DivideLog2Ceiling(frameBuffer.Width, subsamplingX);
int chromaHeight = Av1Math.DivideLog2Ceiling(frameBuffer.Height, subsamplingY);
int chromaOriginX = frameBuffer.OriginX >> subsamplingX;
int chromaOriginY = frameBuffer.OriginY >> subsamplingY;
ExtendPlane(frameBuffer, frameBuffer.BufferCb!, chromaOriginX, chromaOriginY, chromaWidth, chromaHeight);
ExtendPlane(frameBuffer, frameBuffer.BufferCr!, chromaOriginX, chromaOriginY, chromaWidth, chromaHeight);
}
}
/// <summary>
/// Selects the native sample representation for one byte-backed plane.
/// </summary>
/// <param name="frameBuffer">The frame that defines the native sample size.</param>
/// <param name="buffer">The padded plane allocation.</param>
/// <param name="originX">The horizontal visible origin in plane samples.</param>
/// <param name="originY">The vertical visible origin in rows.</param>
/// <param name="width">The visible plane width.</param>
/// <param name="height">The visible plane height.</param>
private static void ExtendPlane(
Av1FrameBuffer<byte> frameBuffer,
Buffer2D<byte> buffer,
int originX,
int originY,
int width,
int height)
{
if (frameBuffer.BytesPerSample == 2)
{
ExtendPlane(MemoryMarshal.Cast<byte, ushort>(buffer.DangerousGetSingleSpan()), buffer.Width >> 1, originX, originY, width, height);
}
else
{
ExtendPlane(buffer.DangerousGetSingleSpan(), buffer.Width, originX, originY, width, height);
}
}
/// <summary>
/// Extends one native sample plane horizontally and then vertically.
/// </summary>
/// <typeparam name="TSample">The native eight-bit or high-bit-depth sample type.</typeparam>
/// <param name="plane">The complete padded plane allocation.</param>
/// <param name="stride">The number of native samples between adjacent rows.</param>
/// <param name="originX">The horizontal visible origin in plane samples.</param>
/// <param name="originY">The vertical visible origin in rows.</param>
/// <param name="width">The visible plane width.</param>
/// <param name="height">The visible plane height.</param>
private static void ExtendPlane<TSample>(Span<TSample> plane, int stride, int originX, int originY, int width, int height)
where TSample : unmanaged
{
int rightStart = originX + width;
int rightLength = stride - rightStart;
for (int row = 0; row < height; row++)
{
Span<TSample> destinationRow = plane.Slice((originY + row) * stride, stride);
// Span.Fill maps these long constant runs to the runtime's vectorized fill implementation. Extending the
// horizontal edges first also makes each later full-row copy include complete left and right padding.
destinationRow[..originX].Fill(destinationRow[originX]);
destinationRow.Slice(rightStart, rightLength).Fill(destinationRow[rightStart - 1]);
}
ReadOnlySpan<TSample> firstVisibleRow = plane.Slice(originY * stride, stride);
for (int row = 0; row < originY; row++)
{
firstVisibleRow.CopyTo(plane.Slice(row * stride, stride));
}
int bottomStart = originY + height;
ReadOnlySpan<TSample> lastVisibleRow = plane.Slice((bottomStart - 1) * stride, stride);
for (int row = bottomStart; row < plane.Length / stride; row++)
{
lastVisibleRow.CopyTo(plane.Slice(row * stride, stride));
}
}
}

279
src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameDerivation.cs

@ -0,0 +1,279 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
/// <summary>
/// Derives the seven AV1 inter-reference map indices from short reference signaling.
/// </summary>
internal static class Av1ReferenceFrameDerivation
{
/// <summary>
/// The shifted-order sentinel used for a reference-map slot that is not available to the current frame.
/// </summary>
private const int UnavailableSortIndex = -1;
/// <summary>
/// The reference-type value subtracted when indexing the seven-entry inter-reference map.
/// </summary>
private const int ReferenceIndexOffset = (int)Av1ReferenceFrameType.Last;
/// <summary>
/// Gets the order in which unassigned backward roles are replaced by forward references.
/// </summary>
private static ReadOnlySpan<Av1ReferenceFrameType> RemainingReferenceOrder =>
[
Av1ReferenceFrameType.Last2,
Av1ReferenceFrameType.Last3,
Av1ReferenceFrameType.Backward,
Av1ReferenceFrameType.Alternate2,
Av1ReferenceFrameType.Alternate,
];
/// <summary>
/// Derives the reference-map slot selected for each inter-reference type when an AV1 frame uses short reference
/// signaling.
/// </summary>
/// <param name="currentOrderHint">The current frame order hint in the active modulo order-hint domain.</param>
/// <param name="orderHintBitWidth">The number of bits in the active order-hint domain.</param>
/// <param name="lastFrameIndex">The explicitly signaled reference-map slot for <see cref="Av1ReferenceFrameType.Last"/>.</param>
/// <param name="goldenFrameIndex">The explicitly signaled reference-map slot for <see cref="Av1ReferenceFrameType.Golden"/>.</param>
/// <param name="slotOrderHints">The eight persisted reference-map order hints.</param>
/// <param name="slotOccupancy">
/// The eight values indicating whether each persisted reference-map slot owns a decoded frame. An empty slot is
/// excluded from derivation.
/// </param>
/// <param name="referenceFrameIndices">
/// The destination for seven slot indices ordered from <see cref="Av1ReferenceFrameType.Last"/> through
/// <see cref="Av1ReferenceFrameType.Alternate"/>.
/// </param>
/// <exception cref="InvalidImageContentException">
/// The signaled LAST or GOLDEN slot is empty, refers to the current frame, or refers to a future frame.
/// </exception>
/// <remarks>
/// The caller owns the fixed AV1 table-size invariants: <paramref name="slotOrderHints"/> and
/// <paramref name="slotOccupancy"/> contain eight entries, while <paramref name="referenceFrameIndices"/> contains
/// seven entries. The signaled indices and order hints have already been read from their bounded bit fields. Every
/// destination entry is overwritten on success, and multiple reference types may select the same slot. Frame-ID
/// validity is a separate conformance state that the caller checks for every resolved reference after derivation.
/// </remarks>
public static void DeriveShortSignaledReferences(
uint currentOrderHint,
int orderHintBitWidth,
uint lastFrameIndex,
uint goldenFrameIndex,
ReadOnlySpan<uint> slotOrderHints,
ReadOnlySpan<bool> slotOccupancy,
Span<uint> referenceFrameIndices)
{
int lastMapIndex = (int)lastFrameIndex;
int goldenMapIndex = (int)goldenFrameIndex;
if (!slotOccupancy[lastMapIndex])
{
// Unlike an unused empty slot, the explicitly signaled LAST slot must own a decoded frame before any
// derived mapping can be consumed. libaom rejects the missing reference at this frame-header boundary.
throw new InvalidImageContentException("An AV1 inter frame requests an unavailable LAST reference.");
}
if (!slotOccupancy[goldenMapIndex])
{
// GOLDEN is the other explicitly signaled slot and has the same ownership requirement as LAST.
throw new InvalidImageContentException("An AV1 inter frame requests an unavailable GOLDEN reference.");
}
int currentFrameSortIndex = 1 << (orderHintBitWidth - 1);
int orderHintMask = currentFrameSortIndex - 1;
InlineArray8<ReferenceFrameInfo> referenceInfo = default;
int lastFrameSortIndex = UnavailableSortIndex;
int goldenFrameSortIndex = UnavailableSortIndex;
for (int mapIndex = 0; mapIndex < Av1Constants.ReferenceFrameCount; mapIndex++)
{
ref ReferenceFrameInfo info = ref referenceInfo[mapIndex];
info.MapIndex = mapIndex;
info.SortIndex = UnavailableSortIndex;
if (!slotOccupancy[mapIndex])
{
// libaom gives absent reference buffers sort index -1. Keeping empty managed slots in the same
// leading partition prevents their stale order hints from participating in temporal selection.
continue;
}
int difference = (int)slotOrderHints[mapIndex] - (int)currentOrderHint;
// get_relative_dist folds the unsigned order-hint difference into the signed half-open interval
// [-2^(bits-1), 2^(bits-1)). Adding the half-range makes -1 available as the absence sentinel while valid
// entries sort from zero through the complete modulo domain.
difference = (difference & orderHintMask) - (difference & currentFrameSortIndex);
info.SortIndex = currentFrameSortIndex + difference;
if (mapIndex == lastMapIndex)
{
lastFrameSortIndex = info.SortIndex;
}
if (mapIndex == goldenMapIndex)
{
goldenFrameSortIndex = info.SortIndex;
}
}
if (lastFrameSortIndex >= currentFrameSortIndex)
{
throw new InvalidImageContentException("An AV1 inter frame requests a current or future frame as LAST.");
}
if (goldenFrameSortIndex >= currentFrameSortIndex)
{
throw new InvalidImageContentException("An AV1 inter frame requests a current or future frame as GOLDEN.");
}
// libaom sorts first by shifted output order and then by reference-map index. The explicit tie break is
// normative: equal order hints select the highest map index for latest references and the lowest for earliest
// references. Insertion sort is bounded to eight inline entries and does not allocate or require general sort
// infrastructure at the frame-header boundary.
for (int index = 1; index < Av1Constants.ReferenceFrameCount; index++)
{
ReferenceFrameInfo current = referenceInfo[index];
int insertionIndex = index;
while (insertionIndex > 0)
{
ReferenceFrameInfo previous = referenceInfo[insertionIndex - 1];
if (previous.SortIndex < current.SortIndex ||
(previous.SortIndex == current.SortIndex && previous.MapIndex <= current.MapIndex))
{
break;
}
referenceInfo[insertionIndex] = previous;
insertionIndex--;
}
referenceInfo[insertionIndex] = current;
}
InlineArray8<bool> assignedReferences = default;
int lastReferenceIndex = (int)Av1ReferenceFrameType.Last - ReferenceIndexOffset;
int goldenReferenceIndex = (int)Av1ReferenceFrameType.Golden - ReferenceIndexOffset;
referenceFrameIndices[lastReferenceIndex] = lastFrameIndex;
referenceFrameIndices[goldenReferenceIndex] = goldenFrameIndex;
assignedReferences[lastReferenceIndex] = true;
assignedReferences[goldenReferenceIndex] = true;
int forwardStartIndex = 0;
int forwardEndIndex = Av1Constants.ReferenceFrameCount - 1;
// Empty entries sort before every occupied shifted hint. The first current-or-future entry then divides the
// remaining sorted table into forward references on the left and backward references on the right.
for (int index = 0; index < Av1Constants.ReferenceFrameCount; index++)
{
if (referenceInfo[index].SortIndex == UnavailableSortIndex)
{
forwardStartIndex++;
continue;
}
if (referenceInfo[index].SortIndex >= currentFrameSortIndex)
{
forwardEndIndex = index - 1;
break;
}
}
int backwardStartIndex = forwardEndIndex + 1;
int backwardEndIndex = Av1Constants.ReferenceFrameCount - 1;
int alternateReferenceIndex = (int)Av1ReferenceFrameType.Alternate - ReferenceIndexOffset;
int backwardReferenceIndex = (int)Av1ReferenceFrameType.Backward - ReferenceIndexOffset;
int alternate2ReferenceIndex = (int)Av1ReferenceFrameType.Alternate2 - ReferenceIndexOffset;
if (backwardStartIndex <= backwardEndIndex)
{
// ALTREF receives the frame farthest into the future. The sorted-map-index tie break selects the highest
// slot when multiple frames share that order hint, matching both the specification and libaom.
referenceFrameIndices[alternateReferenceIndex] = (uint)referenceInfo[backwardEndIndex].MapIndex;
assignedReferences[alternateReferenceIndex] = true;
backwardEndIndex--;
}
if (backwardStartIndex <= backwardEndIndex)
{
// BWDREF receives the nearest future frame and therefore consumes the low end of the backward partition.
referenceFrameIndices[backwardReferenceIndex] = (uint)referenceInfo[backwardStartIndex].MapIndex;
assignedReferences[backwardReferenceIndex] = true;
backwardStartIndex++;
}
if (backwardStartIndex <= backwardEndIndex)
{
// ALTREF2 receives the next-nearest remaining future frame. No further backward lookup follows, so the
// lower boundary does not need to advance after this assignment.
referenceFrameIndices[alternate2ReferenceIndex] = (uint)referenceInfo[backwardStartIndex].MapIndex;
assignedReferences[alternate2ReferenceIndex] = true;
}
ReadOnlySpan<Av1ReferenceFrameType> remainingReferenceOrder = RemainingReferenceOrder;
int remainingIndex;
for (remainingIndex = 0; remainingIndex < remainingReferenceOrder.Length; remainingIndex++)
{
int referenceIndex = (int)remainingReferenceOrder[remainingIndex] - ReferenceIndexOffset;
if (assignedReferences[referenceIndex])
{
continue;
}
// LAST and GOLDEN were already assigned explicitly and cannot be reused while an unassigned forward slot
// remains. Moving from the high end chooses the remaining frames in anti-chronological order.
while (forwardStartIndex <= forwardEndIndex &&
(referenceInfo[forwardEndIndex].MapIndex == lastMapIndex ||
referenceInfo[forwardEndIndex].MapIndex == goldenMapIndex))
{
forwardEndIndex--;
}
if (forwardStartIndex > forwardEndIndex)
{
break;
}
referenceFrameIndices[referenceIndex] = (uint)referenceInfo[forwardEndIndex].MapIndex;
assignedReferences[referenceIndex] = true;
forwardEndIndex--;
}
for (; remainingIndex < remainingReferenceOrder.Length; remainingIndex++)
{
int referenceIndex = (int)remainingReferenceOrder[remainingIndex] - ReferenceIndexOffset;
if (assignedReferences[referenceIndex])
{
continue;
}
// AV1 requires every unfilled role to reuse the earliest available forward frame. At least LAST and GOLDEN
// are occupied forward references, so forwardStartIndex always identifies a usable slot at this point.
referenceFrameIndices[referenceIndex] = (uint)referenceInfo[forwardStartIndex].MapIndex;
assignedReferences[referenceIndex] = true;
}
}
/// <summary>
/// Stores one reference-map slot and its shifted order for fixed-size sorting.
/// </summary>
private struct ReferenceFrameInfo
{
/// <summary>
/// Gets or sets the zero-based slot in the eight-entry persisted reference map.
/// </summary>
public int MapIndex { get; set; }
/// <summary>
/// Gets or sets the order hint shifted around the current frame, or <see cref="UnavailableSortIndex"/> when unavailable.
/// </summary>
public int SortIndex { get; set; }
}
}

265
src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameStore.cs

@ -0,0 +1,265 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
/// <summary>
/// Owns the reference map and selected presentation output for one bounded AV1 decoder session.
/// </summary>
/// <remarks>
/// Several slots and the selected output may identify the same <see cref="Av1ReferenceFrame"/>. The store preserves
/// that sharing without allocating reference-count objects and releases a frame only after its final owning reference
/// has been replaced or cleared. This type is not thread safe; one decoder session serializes commit and disposal.
/// </remarks>
internal sealed class Av1ReferenceFrameStore : IDisposable
{
/// <summary>
/// The number of reference slots defined by the AV1 uncompressed frame header.
/// </summary>
private const int SlotCount = Av1Constants.ReferenceFrameCount;
/// <summary>
/// Stores the frame owner selected by each reference-map slot without allocating a managed array.
/// </summary>
private InlineArray8<Av1ReferenceFrame?> frames;
/// <summary>
/// The most recent shown frame retained for presentation at the end of the bounded image payload.
/// </summary>
private Av1ReferenceFrame? outputFrame;
/// <summary>
/// Gets the most recent shown frame retained for presentation.
/// </summary>
public Av1ReferenceFrame? OutputFrame => this.outputFrame;
/// <summary>
/// Resolves one reference-map slot.
/// </summary>
/// <param name="slot">The zero-based reference-map slot in the inclusive range 0 through 7.</param>
/// <returns>The retained frame, or <see langword="null"/> when the slot has not been populated.</returns>
public Av1ReferenceFrame? Resolve(int slot) => this.frames[slot];
/// <summary>
/// Writes whether each reference-map slot currently owns a reconstructed frame.
/// </summary>
/// <param name="destination">The eight-entry destination receiving the current slot occupancy.</param>
public void FillOccupancy(Span<bool> destination)
{
// Physical ownership is intentionally independent from frame-ID validity. Short reference signaling sorts
// every occupied slot first, then the uncompressed-header parser validates each derived role separately.
for (int slot = 0; slot < SlotCount; slot++)
{
destination[slot] = this.frames[slot] is not null;
}
}
/// <summary>
/// Commits a completed frame to the reference map and, when shown, retains it for presentation.
/// </summary>
/// <param name="refreshFrameFlags">
/// The mask whose bit <c>n</c> replaces reference-map slot <c>n</c>. Only the low eight bits describe AV1 slots.
/// </param>
/// <param name="frame">The completed frame to retain in every selected ownership role.</param>
/// <param name="showFrame">Whether the completed frame replaces the previously retained presentation output.</param>
/// <returns>
/// <see langword="true"/> when the frame is retained as a reference or presentation output and ownership transfers
/// to this store; otherwise <see langword="false"/>, in which case no state changes and the caller retains ownership.
/// </returns>
/// <remarks>
/// The caller must invoke this method only after reconstruction and all normative in-loop filters have completed.
/// Once ownership transfers, the caller must not dispose the frame. A frame passed here must not already be owned by
/// this store.
/// </remarks>
public bool Commit(uint refreshFrameFlags, Av1ReferenceFrame frame, bool showFrame)
{
refreshFrameFlags &= byte.MaxValue;
if (refreshFrameFlags == 0 && !showFrame)
{
// A hidden frame with a zero refresh mask has no remaining role in an image-decoder session.
return false;
}
InlineArray8<Av1ReferenceFrame?> replacedFrames = default;
Av1ReferenceFrame? replacedOutputFrame = showFrame ? this.outputFrame : null;
// Capture displaced owners in inline storage, then publish the complete slot and output transition before
// releasing anything. A shown frame may also occupy reference slots, so both ownership domains must change as
// one operation.
for (int slot = 0; slot < SlotCount; slot++)
{
if ((refreshFrameFlags & (1U << slot)) != 0)
{
replacedFrames[slot] = this.frames[slot];
this.frames[slot] = frame;
}
}
if (showFrame)
{
this.outputFrame = frame;
}
for (int replacedIndex = 0; replacedIndex < SlotCount; replacedIndex++)
{
Av1ReferenceFrame? replacedFrame = replacedFrames[replacedIndex];
if (replacedFrame is null)
{
continue;
}
if (ReferenceEquals(replacedFrame, replacedOutputFrame))
{
// Let the displaced-output path release this shared owner after every slot candidate has been removed.
replacedFrames[replacedIndex] = null;
continue;
}
// A displaced frame remains owned when any unrefreshed slot or the selected output still references it.
// Eight fixed slots make the bounded identity scan cheaper than allocated reference-count state.
if (this.IsRetained(replacedFrame))
{
replacedFrames[replacedIndex] = null;
}
}
DisposeUnique(ref replacedFrames);
if (replacedOutputFrame is not null && !this.IsRetained(replacedOutputFrame))
{
replacedOutputFrame.Dispose();
}
return true;
}
/// <summary>
/// Replaces the selected presentation output with an independently owned completed frame.
/// </summary>
/// <param name="frame">The completed presentation frame whose ownership transfers to this store.</param>
/// <remarks>
/// This path is used when film grain requires presentation samples to differ from the ungrained reconstruction
/// retained by the reference map.
/// </remarks>
public void CommitOutput(Av1ReferenceFrame frame)
{
Av1ReferenceFrame? replacedFrame = this.outputFrame;
this.outputFrame = frame;
// The previous output may still be retained by one or more reference slots. Release it only after publishing
// the new output and confirming that no reference-map identity remains.
if (replacedFrame is not null && !this.IsRetained(replacedFrame))
{
replacedFrame.Dispose();
}
}
/// <summary>
/// Transfers the selected presentation frame out of this store and releases every other retained frame.
/// </summary>
/// <returns>The selected presentation frame now owned by the caller.</returns>
public Av1ReferenceFrame TakeOutput()
{
Av1ReferenceFrame result = this.outputFrame!;
this.outputFrame = null;
// The caller becomes the sole owner of the selected output. Remove all slot aliases before Reset releases the
// remaining session references so the sample buffer can transfer without copying.
for (int slot = 0; slot < SlotCount; slot++)
{
if (ReferenceEquals(this.frames[slot], result))
{
this.frames[slot] = null;
}
}
this.Reset();
return result;
}
/// <summary>
/// Clears all reference-map slots and releases every uniquely retained frame.
/// </summary>
public void Reset()
{
InlineArray8<Av1ReferenceFrame?> releasedFrames = this.frames;
this.frames = default;
Av1ReferenceFrame? releasedOutputFrame = this.outputFrame;
this.outputFrame = null;
// Clear the live map before disposal so the store cannot expose a partially reset ownership state. When the
// output aliases a slot, let the output path perform the single release after the duplicate slot is removed.
if (releasedOutputFrame is not null)
{
for (int slot = 0; slot < SlotCount; slot++)
{
if (ReferenceEquals(releasedFrames[slot], releasedOutputFrame))
{
releasedFrames[slot] = null;
}
}
}
DisposeUnique(ref releasedFrames);
releasedOutputFrame?.Dispose();
}
/// <summary>
/// Releases every uniquely retained frame and clears all reference-map slots.
/// </summary>
public void Dispose() => this.Reset();
/// <summary>
/// Determines whether the live reference map or presentation output retains a frame.
/// </summary>
/// <param name="frame">The frame whose ownership is queried.</param>
/// <returns><see langword="true"/> when the store still owns the frame.</returns>
private bool IsRetained(Av1ReferenceFrame frame)
{
if (ReferenceEquals(this.outputFrame, frame))
{
return true;
}
for (int slot = 0; slot < SlotCount; slot++)
{
if (ReferenceEquals(this.frames[slot], frame))
{
return true;
}
}
return false;
}
/// <summary>
/// Releases each distinct frame owner in a fixed-size set exactly once.
/// </summary>
/// <param name="frames">The inline set of frame references to release.</param>
private static void DisposeUnique(ref InlineArray8<Av1ReferenceFrame?> frames)
{
for (int frameIndex = 0; frameIndex < SlotCount; frameIndex++)
{
Av1ReferenceFrame? frame = frames[frameIndex];
if (frame is null)
{
continue;
}
// Null every later alias before disposal. The store intentionally represents shared slot ownership through
// object identity, so no separately allocated reference-count state is needed for the eight-entry map.
for (int duplicateIndex = frameIndex + 1; duplicateIndex < SlotCount; duplicateIndex++)
{
if (ReferenceEquals(frames[duplicateIndex], frame))
{
frames[duplicateIndex] = null;
}
}
frames[frameIndex] = null;
frame.Dispose();
}
}
}

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

@ -1,16 +1,33 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// <summary>
/// Stores block-size, prediction-mode, transform, and palette decisions shared by AV1 block processing.
/// Stores block-size, intra/inter prediction, transform, and palette decisions shared by AV1 block processing.
/// </summary>
internal class Av1BlockModeInfo
{
/// <summary>
/// Stores the primary and optional secondary reference-frame labels.
/// </summary>
private InlineArray2<Av1ReferenceFrameType> referenceFrames;
/// <summary>
/// Stores the motion vector associated with each reference-frame label.
/// </summary>
private InlineArray2<Av1MotionVector> motionVectors;
/// <summary>
/// Stores the vertical and horizontal subpixel interpolation filters in that order.
/// </summary>
private InlineArray2<Av1InterpolationFilter> interpolationFilters;
/// <summary>
/// The palette size for the luma plane.
/// </summary>
@ -85,6 +102,11 @@ internal class Av1BlockModeInfo
{
this.BlockSize = blockSize;
this.PositionInSuperblock = positionInSuperblock;
// Both entries begin absent because inter syntax has not selected either reference yet. Intra parsing replaces
// the primary entry with the current frame while retaining None as the optional secondary reference.
this.referenceFrames[0] = Av1ReferenceFrameType.None;
this.referenceFrames[1] = Av1ReferenceFrameType.None;
}
/// <summary>
@ -97,6 +119,89 @@ internal class Av1BlockModeInfo
/// </summary>
public Av1PredictionMode YMode { get; set; }
/// <summary>
/// Gets the primary and optional secondary reference-frame labels.
/// </summary>
/// <remarks>
/// Index zero is the primary reference. Index one is <see cref="Av1ReferenceFrameType.None"/> for a single-reference
/// block, <see cref="Av1ReferenceFrameType.Intra"/> for an inter-intra block, or the secondary inter-reference label
/// for compound prediction.
/// </remarks>
public Span<Av1ReferenceFrameType> ReferenceFrames => this.referenceFrames;
/// <summary>
/// Gets the decoded motion vectors corresponding to <see cref="ReferenceFrames"/>.
/// </summary>
public Span<Av1MotionVector> MotionVectors => this.motionVectors;
/// <summary>
/// Gets the interpolation filters used for vertical and horizontal subpixel prediction.
/// </summary>
/// <remarks>
/// Index zero is the vertical filter and index one is the horizontal filter, matching libaom's
/// <c>InterpFilters.y_filter</c> and <c>InterpFilters.x_filter</c> layout.
/// </remarks>
public Span<Av1InterpolationFilter> InterpolationFilters => this.interpolationFilters;
/// <summary>
/// Gets or sets the selected index in the derived reference-motion-vector stack.
/// </summary>
/// <remarks>
/// The AV1 syntax constrains this value to the inclusive range 0 through 3.
/// </remarks>
public byte ReferenceMotionVectorIndex { get; set; }
/// <summary>
/// Gets or sets the motion model used to construct inter prediction.
/// </summary>
public Av1MotionMode MotionMode { get; set; }
/// <summary>
/// Gets or sets the intra predictor blended with a single-reference inter predictor.
/// </summary>
public Av1InterIntraMode InterIntraMode { get; set; }
/// <summary>
/// Gets or sets a value indicating whether inter-intra prediction uses a wedge mask.
/// </summary>
public bool UseInterIntraWedge { get; set; }
/// <summary>
/// Gets or sets the inter-intra wedge-mask index in the inclusive range 0 through 15.
/// </summary>
public byte InterIntraWedgeIndex { get; set; }
/// <summary>
/// Gets or sets a value indicating whether compound prediction uses the masked-compound mode group.
/// </summary>
public bool CompoundGroupIndex { get; set; }
/// <summary>
/// Gets or sets a value indicating whether unmasked compound prediction uses average blending.
/// A value of <see langword="false"/> selects distance-weighted blending.
/// </summary>
public bool CompoundIndex { get; set; }
/// <summary>
/// Gets or sets the compound blending method selected for two inter predictors.
/// </summary>
public Av1CompoundType CompoundType { get; set; }
/// <summary>
/// Gets or sets the compound wedge-mask index in the inclusive range 0 through 15.
/// </summary>
public byte CompoundWedgeIndex { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the compound wedge mask is inverted.
/// </summary>
public bool CompoundWedgeSign { get; set; }
/// <summary>
/// Gets or sets the orientation of the difference-weighted compound mask.
/// </summary>
public Av1DifferenceWeightedMaskType DifferenceWeightedMaskType { get; set; }
/// <summary>
/// Gets or sets a value indicating whether residual coefficients are omitted for the block.
/// </summary>
@ -118,9 +223,9 @@ internal class Av1BlockModeInfo
public int SegmentId { get; set; }
/// <summary>
/// Gets or sets the <see cref="Av1PredictionMode"/> for the chroma channels.
/// Gets or sets the chroma intra-prediction mode.
/// </summary>
public Av1PredictionMode UvMode { get; set; }
public Av1ChromaPredictionMode UvMode { get; set; }
/// <summary>
/// Gets or sets a value indicating whether intra block copy is selected.
@ -334,4 +439,17 @@ internal class Av1BlockModeInfo
this.chromaPaletteColorIndexMap = colorIndexMap;
}
}
/// <summary>
/// Provides fixed storage for the two values associated with AV1's primary and secondary inter references.
/// </summary>
/// <typeparam name="T">The stored reference label, motion vector, or interpolation-filter type.</typeparam>
[InlineArray(2)]
private struct InlineArray2<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
}

30
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1CompoundType.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// <summary>
/// Identifies the blending method used to combine two AV1 inter predictors.
/// </summary>
internal enum Av1CompoundType : byte
{
/// <summary>
/// Averages both predictors with equal weights.
/// </summary>
Average = 0,
/// <summary>
/// Weights predictors from their relative display-order distances.
/// </summary>
DistanceWeighted = 1,
/// <summary>
/// Selects per-pixel weights from a signaled wedge mask.
/// </summary>
Wedge = 2,
/// <summary>
/// Derives per-pixel weights from the difference between both predictors.
/// </summary>
DifferenceWeighted = 3,
}

20
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1DifferenceWeightedMaskType.cs

@ -0,0 +1,20 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// <summary>
/// Identifies the orientation of an AV1 difference-weighted compound mask.
/// </summary>
internal enum Av1DifferenceWeightedMaskType : byte
{
/// <summary>
/// Applies the predictor-difference adjustment to a base alpha weight of 38 on AV1's 0-through-64 blend scale.
/// </summary>
Type38 = 0,
/// <summary>
/// Applies the complement of the type-38 predictor-difference mask.
/// </summary>
Type38Inverse = 1,
}

4
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderBlockModeInfo.cs

@ -28,7 +28,7 @@ internal class Av1EncoderBlockModeInfo
/// <summary>
/// Gets the selected chroma prediction mode.
/// </summary>
public Av1PredictionMode UvPredictionMode { get; }
public Av1ChromaPredictionMode UvPredictionMode { get; }
/// <summary>
/// Gets a value indicating whether residual coefficients are omitted for the block.
@ -63,5 +63,5 @@ internal class Av1EncoderBlockModeInfo
/// <summary>
/// Gets or sets the chroma prediction mode written for the block.
/// </summary>
public Av1PredictionMode UvMode { get; internal set; }
public Av1ChromaPredictionMode UvMode { get; internal set; }
}

536
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.MotionField.cs

@ -0,0 +1,536 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// <summary>
/// Owns the retained and projected per-8x8 motion fields associated with a decoded AV1 frame.
/// </summary>
internal partial class Av1FrameInfo
{
/// <summary>
/// The maximum absolute temporal distance accepted by AV1 motion-field projection.
/// </summary>
private const int MaximumFrameDistance = 31;
/// <summary>
/// The maximum number of reference frames projected into one temporal motion field.
/// </summary>
private const int MotionFieldProjectionCount = 3;
/// <summary>
/// The maximum source motion-vector magnitude retained for later temporal projection.
/// </summary>
private const int ReferenceMotionVectorLimit = 4095;
/// <summary>
/// The exclusive upper bound of an AV1 motion-vector component in one-eighth-sample units.
/// </summary>
private const int MotionVectorUpperBound = 16384;
/// <summary>
/// The reserved lower endpoint of an AV1 motion-vector component in one-eighth-sample units.
/// </summary>
private const int MotionVectorLowerBound = -16384;
/// <summary>
/// The width or height of the largest AV1 superblock in 4x4 mode-information units.
/// </summary>
private const int MaximumSuperblockModeInfoSize = 1 << (Av1Constants.MaxSuperBlockSizeLog2 - Av1Constants.ModeInfoSizeLog2);
/// <summary>
/// The base-two logarithm of <see cref="MaximumSuperblockModeInfoSize"/>.
/// </summary>
private const int MaximumSuperblockModeInfoSizeLog2 = Av1Constants.MaxSuperBlockSizeLog2 - Av1Constants.ModeInfoSizeLog2;
/// <summary>
/// The base-two reduction from 4x4 mode-information coordinates to the 8x8 motion-field grid.
/// </summary>
private const int MotionFieldModeInfoShift = 1;
/// <summary>
/// The base-two reduction from one-eighth-sample motion vectors to offsets on the 8x8 motion-field grid.
/// </summary>
private const int MotionVectorToFieldOffsetShift = 4 + Av1Constants.ModeInfoSizeLog2;
/// <summary>
/// The maximum horizontal projection displacement, measured in 8x8 motion-field blocks.
/// </summary>
private const int MaximumHorizontalFieldOffset = 8;
/// <summary>
/// Stores the selected motion vector and logical reference for every retained 8x8 frame position.
/// </summary>
private RetainedMotionFieldEntry[] retainedMotionField = [];
/// <summary>
/// Stores motion vectors projected from retained frames into the current frame's 8x8 grid.
/// </summary>
private TemporalMotionFieldEntry[] temporalMotionField = [];
/// <summary>
/// Stores the order hint selected by each logical inter-reference type for later projections from this frame.
/// </summary>
private InlineArray8<uint> motionFieldReferenceOrderHints;
/// <summary>
/// Stores whether each logical inter-reference type lies after, at, or before the current frame in display order.
/// </summary>
private InlineArray8<sbyte> motionFieldReferenceSides;
/// <summary>
/// The number of retained motion-field entries in one active 8x8 row.
/// </summary>
private int retainedMotionFieldStride;
/// <summary>
/// The number of projected temporal-motion entries in one aligned 8x8 row.
/// </summary>
private int temporalMotionFieldStride;
/// <summary>
/// The active frame width in 4x4 mode-information units.
/// </summary>
private int activeModeInfoColumnCount;
/// <summary>
/// The active frame height in 4x4 mode-information units.
/// </summary>
private int activeModeInfoRowCount;
/// <summary>
/// Gets the reciprocal table used by AV1 motion-vector projection in 14-bit fixed-point precision.
/// </summary>
private static ReadOnlySpan<int> ProjectionDivisors =>
[0, 16384, 8192, 5461, 4096, 3276, 2730, 2340, 2048, 1820, 1638, 1489, 1365, 1260, 1170, 1092,
1024, 963, 910, 862, 819, 780, 744, 712, 682, 655, 630, 606, 585, 564, 546, 528];
/// <summary>
/// Allocates and derives the motion fields required by one decoded frame.
/// </summary>
/// <param name="sequenceHeader">The sequence header defining motion-field enablement and order-hint precision.</param>
/// <param name="frameHeader">The current frame header and its seven resolved inter-reference roles.</param>
/// <param name="referenceFrames">The retained reconstructed frames selected by the current reference map.</param>
public void InitializeMotionField(
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader,
Av1ReferenceFrameStore referenceFrames)
{
if (!sequenceHeader.OrderHintInfo.EnableReferenceFrameMotionVectors)
{
return;
}
this.activeModeInfoColumnCount = frameHeader.ModeInfoColumnCount;
this.activeModeInfoRowCount = frameHeader.ModeInfoRowCount;
this.retainedMotionFieldStride = (this.activeModeInfoColumnCount + 1) >> MotionFieldModeInfoShift;
if (frameHeader.IsIntra)
{
// Intra frames retain an empty source field. They can occupy reference slots, but libaom rejects them as
// projection sources before consulting their reference-order-hint metadata.
return;
}
InlineArray8<Av1ReferenceFrame?> selectedReferences = default;
Span<uint> referenceFrameIndices = frameHeader.GetReferenceFrameIndices();
int orderHintBits = sequenceHeader.OrderHintInfo.OrderHintBits;
// Capture the seven logical-role order hints before this frame refreshes any physical map slots. Libaom keeps
// the same snapshot on RefCntBuffer so a later frame can project this frame's stored motion vectors.
for (int referenceIndex = 0; referenceIndex < Av1Constants.ReferencesPerFrame; referenceIndex++)
{
Av1ReferenceFrameType referenceFrameType = (Av1ReferenceFrameType)(referenceIndex + 1);
Av1ReferenceFrame referenceFrame = referenceFrames.Resolve((int)referenceFrameIndices[referenceIndex])!;
uint referenceOrderHint = referenceFrame.FrameHeader.OrderHint;
selectedReferences[(int)referenceFrameType] = referenceFrame;
this.motionFieldReferenceOrderHints[(int)referenceFrameType] = referenceOrderHint;
int relativeDistance = GetRelativeDistance(referenceOrderHint, frameHeader.OrderHint, orderHintBits);
this.motionFieldReferenceSides[(int)referenceFrameType] = relativeDistance > 0
? (sbyte)1
: referenceOrderHint == frameHeader.OrderHint ? (sbyte)-1 : (sbyte)0;
}
// FrameInfo is transferred directly into each retained frame owner, so allocate only the active 8x8 source
// grid whose completed block vectors can be projected by a later frame.
int retainedRowCount = (this.activeModeInfoRowCount + 1) >> MotionFieldModeInfoShift;
this.retainedMotionField = new RetainedMotionFieldEntry[this.retainedMotionFieldStride * retainedRowCount];
if (!frameHeader.UseReferenceFrameMotionVectors)
{
return;
}
// libaom aligns the projected field stride to the largest superblock even for a 64x64 sequence. This keeps
// later temporal-candidate addressing independent of the current sequence's selected superblock size.
int alignedModeInfoColumnCount = Av1Math.AlignPowerOf2(
this.activeModeInfoColumnCount,
MaximumSuperblockModeInfoSizeLog2);
this.temporalMotionFieldStride = alignedModeInfoColumnCount >> MotionFieldModeInfoShift;
int temporalRowCount = (this.activeModeInfoRowCount + MaximumSuperblockModeInfoSize) >> MotionFieldModeInfoShift;
this.temporalMotionField = new TemporalMotionFieldEntry[this.temporalMotionFieldStride * temporalRowCount];
// AV1 examines LAST, BWDREF, ALTREF2, ALTREF, and LAST2 in this normative order and admits at most three
// projection sources. LAST always consumes the first budget position, forward references consume one only
// when eligible projection succeeds, and LAST2 fills the final unused position in the reverse direction.
int remainingProjectionCount = MotionFieldProjectionCount;
Av1ReferenceFrame lastFrame = selectedReferences[(int)Av1ReferenceFrameType.Last]!;
Av1ReferenceFrame goldenFrame = selectedReferences[(int)Av1ReferenceFrameType.Golden]!;
uint alternateOfLastOrderHint = lastFrame.FrameInfo.motionFieldReferenceOrderHints[(int)Av1ReferenceFrameType.Alternate];
// A LAST frame whose ALTREF order matches GOLDEN is an overlay. Projecting it would duplicate the overlay's
// temporal source, but libaom still consumes one position from the three-source projection budget.
if (alternateOfLastOrderHint != goldenFrame.FrameHeader.OrderHint)
{
_ = this.ProjectMotionField(sequenceHeader, frameHeader, lastFrame, reverseDirection: true);
}
remainingProjectionCount--;
Av1ReferenceFrame backwardFrame = selectedReferences[(int)Av1ReferenceFrameType.Backward]!;
if (GetRelativeDistance(backwardFrame.FrameHeader.OrderHint, frameHeader.OrderHint, orderHintBits) > 0 &&
this.ProjectMotionField(sequenceHeader, frameHeader, backwardFrame, reverseDirection: false))
{
remainingProjectionCount--;
}
Av1ReferenceFrame alternate2Frame = selectedReferences[(int)Av1ReferenceFrameType.Alternate2]!;
if (GetRelativeDistance(alternate2Frame.FrameHeader.OrderHint, frameHeader.OrderHint, orderHintBits) > 0 &&
this.ProjectMotionField(sequenceHeader, frameHeader, alternate2Frame, reverseDirection: false))
{
remainingProjectionCount--;
}
Av1ReferenceFrame alternateFrame = selectedReferences[(int)Av1ReferenceFrameType.Alternate]!;
if (remainingProjectionCount > 0 &&
GetRelativeDistance(alternateFrame.FrameHeader.OrderHint, frameHeader.OrderHint, orderHintBits) > 0 &&
this.ProjectMotionField(sequenceHeader, frameHeader, alternateFrame, reverseDirection: false))
{
remainingProjectionCount--;
}
if (remainingProjectionCount > 0)
{
Av1ReferenceFrame last2Frame = selectedReferences[(int)Av1ReferenceFrameType.Last2]!;
_ = this.ProjectMotionField(sequenceHeader, frameHeader, last2Frame, reverseDirection: true);
}
}
/// <summary>
/// Gets the temporal motion vector projected over a 4x4 mode-information position.
/// </summary>
/// <param name="modeInfoRow">The zero-based 4x4 row.</param>
/// <param name="modeInfoColumn">The zero-based 4x4 column.</param>
/// <param name="motionVector">Receives the retained source vector in one-eighth-sample units.</param>
/// <param name="referenceFrameOffset">Receives the positive temporal distance from the source to its reference.</param>
/// <returns><see langword="true"/> when a projected vector covers the requested position.</returns>
public bool TryGetTemporalMotionVector(
int modeInfoRow,
int modeInfoColumn,
out Av1MotionVector motionVector,
out int referenceFrameOffset)
{
int index = ((modeInfoRow >> MotionFieldModeInfoShift) * this.temporalMotionFieldStride) +
(modeInfoColumn >> MotionFieldModeInfoShift);
TemporalMotionFieldEntry entry = this.temporalMotionField[index];
motionVector = entry.MotionVector;
referenceFrameOffset = entry.ReferenceFrameOffset;
return referenceFrameOffset > 0;
}
/// <summary>
/// Writes the retained per-8x8 motion-field entries covered by one completed mode-information block.
/// </summary>
/// <param name="modeInfo">The completed block mode information.</param>
/// <param name="modeInfoPosition">The block origin in frame-relative 4x4 units.</param>
private void UpdateRetainedMotionField(Av1BlockModeInfo modeInfo, Point modeInfoPosition)
{
if (this.retainedMotionField.Length == 0)
{
return;
}
Av1ReferenceFrameType selectedReference = Av1ReferenceFrameType.None;
Av1MotionVector selectedMotionVector = default;
Span<Av1ReferenceFrameType> referenceFrames = modeInfo.ReferenceFrames;
Span<Av1MotionVector> motionVectors = modeInfo.MotionVectors;
// Compound blocks may offer two vectors. libaom retains the last eligible forward-or-past reference after
// excluding same-order, future, and out-of-range vectors, so preserve that overwrite order exactly.
for (int referenceIndex = 0; referenceIndex < 2; referenceIndex++)
{
Av1ReferenceFrameType referenceFrame = referenceFrames[referenceIndex];
Av1MotionVector motionVector = motionVectors[referenceIndex];
if (referenceFrame > Av1ReferenceFrameType.Intra &&
this.motionFieldReferenceSides[(int)referenceFrame] == 0 &&
Math.Abs(motionVector.Row) <= ReferenceMotionVectorLimit &&
Math.Abs(motionVector.Column) <= ReferenceMotionVectorLimit)
{
selectedReference = referenceFrame;
selectedMotionVector = motionVector;
}
}
int blockModeInfoWidth = Math.Min(
modeInfo.BlockSize.Get4x4WideCount(),
this.activeModeInfoColumnCount - modeInfoPosition.X);
int blockModeInfoHeight = Math.Min(
modeInfo.BlockSize.Get4x4HighCount(),
this.activeModeInfoRowCount - modeInfoPosition.Y);
int fieldWidth = (blockModeInfoWidth + 1) >> MotionFieldModeInfoShift;
int fieldHeight = (blockModeInfoHeight + 1) >> MotionFieldModeInfoShift;
int firstFieldRow = modeInfoPosition.Y >> MotionFieldModeInfoShift;
int firstFieldColumn = modeInfoPosition.X >> MotionFieldModeInfoShift;
RetainedMotionFieldEntry entry = new(selectedMotionVector, selectedReference);
// One decoded block supplies the same retained candidate to every covered 8x8 cell. Array.Fill preserves the
// native contiguous-row write and lets later sub-8x8 blocks overwrite the shared cell in traversal order.
for (int row = 0; row < fieldHeight; row++)
{
int rowOffset = ((firstFieldRow + row) * this.retainedMotionFieldStride) + firstFieldColumn;
Array.Fill(this.retainedMotionField, entry, rowOffset, fieldWidth);
}
}
/// <summary>
/// Projects one retained frame's motion field into the current frame's temporal candidate grid.
/// </summary>
/// <param name="sequenceHeader">The sequence header defining the modulo order-hint domain.</param>
/// <param name="frameHeader">The current frame header.</param>
/// <param name="startFrame">The retained frame whose stored motion vectors are projected.</param>
/// <param name="reverseDirection">
/// A value indicating whether the start-to-current distance and spatial displacement are reversed for a past frame.
/// </param>
/// <returns><see langword="true"/> when the retained frame is an eligible projection source.</returns>
private bool ProjectMotionField(
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader,
Av1ReferenceFrame startFrame,
bool reverseDirection)
{
ObuFrameHeader startFrameHeader = startFrame.FrameHeader;
if (startFrameHeader.IsIntra ||
startFrameHeader.ModeInfoRowCount != this.activeModeInfoRowCount ||
startFrameHeader.ModeInfoColumnCount != this.activeModeInfoColumnCount)
{
// AV1 does not rescale temporal fields. Intra sources contain no inter motion, and a differently sized
// source has no one-to-one 8x8 grid on which the normative projection can operate.
return false;
}
Av1FrameInfo startFrameInfo = startFrame.FrameInfo;
int orderHintBits = sequenceHeader.OrderHintInfo.OrderHintBits;
int startToCurrentFrameOffset = GetRelativeDistance(
startFrameHeader.OrderHint,
frameHeader.OrderHint,
orderHintBits);
if (reverseDirection)
{
startToCurrentFrameOffset = -startToCurrentFrameOffset;
}
int sourceRowCount = (this.activeModeInfoRowCount + 1) >> MotionFieldModeInfoShift;
int sourceColumnCount = (this.activeModeInfoColumnCount + 1) >> MotionFieldModeInfoShift;
int destinationRowCount = this.activeModeInfoRowCount >> MotionFieldModeInfoShift;
int destinationColumnCount = this.activeModeInfoColumnCount >> MotionFieldModeInfoShift;
for (int blockRow = 0; blockRow < sourceRowCount; blockRow++)
{
int sourceRowOffset = blockRow * startFrameInfo.retainedMotionFieldStride;
for (int blockColumn = 0; blockColumn < sourceColumnCount; blockColumn++)
{
RetainedMotionFieldEntry source = startFrameInfo.retainedMotionField[sourceRowOffset + blockColumn];
if (source.ReferenceFrame <= Av1ReferenceFrameType.Intra)
{
continue;
}
int referenceFrameOffset = GetRelativeDistance(
startFrameHeader.OrderHint,
startFrameInfo.motionFieldReferenceOrderHints[(int)source.ReferenceFrame],
orderHintBits);
bool positionIsValid = Math.Abs(referenceFrameOffset) <= MaximumFrameDistance &&
referenceFrameOffset > 0 &&
Math.Abs(startToCurrentFrameOffset) <= MaximumFrameDistance;
if (!positionIsValid)
{
continue;
}
Av1MotionVector projected = ProjectMotionVector(
source.MotionVector,
startToCurrentFrameOffset,
referenceFrameOffset);
if (!TryGetProjectedBlockPosition(
blockRow,
blockColumn,
projected,
reverseDirection,
destinationRowCount,
destinationColumnCount,
out int projectedRow,
out int projectedColumn))
{
continue;
}
// The projected vector selects the destination cell, but AV1 stores the original forward vector and
// its source-to-reference distance there. Candidate scaling later uses both values for its own target.
int destinationOffset = (projectedRow * this.temporalMotionFieldStride) + projectedColumn;
this.temporalMotionField[destinationOffset] = new(source.MotionVector, referenceFrameOffset);
}
}
return true;
}
/// <summary>
/// Scales a retained motion vector by a signed ratio of temporal distances.
/// </summary>
/// <param name="motionVector">The retained vector in one-eighth-sample units.</param>
/// <param name="numerator">The signed start-to-current temporal distance.</param>
/// <param name="denominator">The positive start-to-reference temporal distance.</param>
/// <returns>The projected and AV1-range-clamped vector.</returns>
private static Av1MotionVector ProjectMotionVector(Av1MotionVector motionVector, int numerator, int denominator)
{
denominator = Math.Min(denominator, MaximumFrameDistance);
numerator = Av1Math.Clip3(-MaximumFrameDistance, MaximumFrameDistance, numerator);
// ProjectionDivisors represents 1 / denominator in Q14. Symmetric power-of-two rounding matches libaom for
// negative vectors, and the final clamp excludes the two reserved extreme motion-vector values.
int row = Av1Math.RoundPowerOf2Signed(motionVector.Row * numerator * ProjectionDivisors[denominator], 14);
int column = Av1Math.RoundPowerOf2Signed(motionVector.Column * numerator * ProjectionDivisors[denominator], 14);
row = Av1Math.Clip3(MotionVectorLowerBound + 1, MotionVectorUpperBound - 1, row);
column = Av1Math.Clip3(MotionVectorLowerBound + 1, MotionVectorUpperBound - 1, column);
return new(row, column);
}
/// <summary>
/// Maps a projected motion vector to its bounded destination on the current 8x8 field.
/// </summary>
/// <param name="blockRow">The source 8x8 row.</param>
/// <param name="blockColumn">The source 8x8 column.</param>
/// <param name="motionVector">The temporally projected vector in one-eighth-sample units.</param>
/// <param name="reverseDirection">Whether the vector moves backwards from the source position.</param>
/// <param name="rowCount">The number of complete 8x8 rows in the current frame.</param>
/// <param name="columnCount">The number of complete 8x8 columns in the current frame.</param>
/// <param name="projectedRow">Receives the projected 8x8 row.</param>
/// <param name="projectedColumn">Receives the projected 8x8 column.</param>
/// <returns><see langword="true"/> when the destination lies in the permitted projection window.</returns>
private static bool TryGetProjectedBlockPosition(
int blockRow,
int blockColumn,
Av1MotionVector motionVector,
bool reverseDirection,
int rowCount,
int columnCount,
out int projectedRow,
out int projectedColumn)
{
int baseBlockRow = (blockRow >> 3) << 3;
int baseBlockColumn = (blockColumn >> 3) << 3;
// One field cell spans 8 samples, while vectors use one-eighth-sample units; dividing by 64 converts between
// them. C# integer division truncates toward zero, matching libaom's explicit signed-shift construction.
int rowOffset = motionVector.Row / (1 << MotionVectorToFieldOffsetShift);
int columnOffset = motionVector.Column / (1 << MotionVectorToFieldOffsetShift);
projectedRow = reverseDirection ? blockRow - rowOffset : blockRow + rowOffset;
projectedColumn = reverseDirection ? blockColumn - columnOffset : blockColumn + columnOffset;
if (projectedRow < 0 || projectedRow >= rowCount || projectedColumn < 0 || projectedColumn >= columnCount)
{
return false;
}
// AV1 keeps a projection in the same 64x64 row band and permits one additional 64-sample horizontal band on
// either side. This bounds temporal-candidate lookup while accommodating common lateral motion.
return projectedRow >= baseBlockRow &&
projectedRow < baseBlockRow + 8 &&
projectedColumn >= baseBlockColumn - MaximumHorizontalFieldOffset &&
projectedColumn < baseBlockColumn + 8 + MaximumHorizontalFieldOffset;
}
/// <summary>
/// Computes the signed distance between two order hints in their modulo domain.
/// </summary>
/// <param name="first">The first order hint.</param>
/// <param name="second">The order hint subtracted from <paramref name="first"/>.</param>
/// <param name="orderHintBits">The number of bits in the order-hint domain.</param>
/// <returns>The shortest signed modulo distance.</returns>
private static int GetRelativeDistance(uint first, uint second, int orderHintBits)
{
int difference = (int)first - (int)second;
int signBit = 1 << (orderHintBits - 1);
return (difference & (signBit - 1)) - (difference & signBit);
}
/// <summary>
/// Stores one motion vector and logical reference retained for projection by a later frame.
/// </summary>
private readonly struct RetainedMotionFieldEntry
{
/// <summary>
/// Initializes a new instance of the <see cref="RetainedMotionFieldEntry"/> struct.
/// </summary>
/// <param name="motionVector">The retained motion vector in one-eighth-sample units.</param>
/// <param name="referenceFrame">The logical reference targeted by the vector.</param>
public RetainedMotionFieldEntry(Av1MotionVector motionVector, Av1ReferenceFrameType referenceFrame)
{
this.MotionVector = motionVector;
this.ReferenceFrame = referenceFrame;
}
/// <summary>
/// Gets the retained motion vector in one-eighth-sample units.
/// </summary>
public Av1MotionVector MotionVector { get; }
/// <summary>
/// Gets the logical reference targeted by <see cref="MotionVector"/>.
/// </summary>
public Av1ReferenceFrameType ReferenceFrame { get; }
}
/// <summary>
/// Stores one temporal candidate projected over the current frame's 8x8 grid.
/// </summary>
private readonly struct TemporalMotionFieldEntry
{
/// <summary>
/// Initializes a new instance of the <see cref="TemporalMotionFieldEntry"/> struct.
/// </summary>
/// <param name="motionVector">The retained source vector in one-eighth-sample units.</param>
/// <param name="referenceFrameOffset">The positive temporal distance from the source to its reference.</param>
public TemporalMotionFieldEntry(Av1MotionVector motionVector, int referenceFrameOffset)
{
this.MotionVector = motionVector;
this.ReferenceFrameOffset = referenceFrameOffset;
}
/// <summary>
/// Gets the retained source vector in one-eighth-sample units.
/// </summary>
public Av1MotionVector MotionVector { get; }
/// <summary>
/// Gets the positive temporal distance from the source frame to its reference.
/// </summary>
public int ReferenceFrameOffset { get; }
}
}

117
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs

@ -6,7 +6,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// <summary>
/// Owns the mode, transform, coefficient, quantizer, and filter state decoded for one AV1 frame.
/// Owns the mode, motion, segmentation, transform, coefficient, quantizer, and filter state decoded for one AV1 frame.
/// </summary>
internal partial class Av1FrameInfo
{
@ -70,6 +70,21 @@ internal partial class Av1FrameInfo
/// </summary>
private readonly Av1FrameModeInfoMap modeInfoMap;
/// <summary>
/// Stores the decoded segment identifier for each active 4x4 mode-information position in row-major order.
/// </summary>
private byte[] segmentIds = [];
/// <summary>
/// The number of active 4x4 columns in one row of <see cref="segmentIds"/>.
/// </summary>
private int segmentIdColumnCount;
/// <summary>
/// The number of active 4x4 rows represented by <see cref="segmentIds"/>.
/// </summary>
private int segmentIdRowCount;
/// <summary>
/// Stores luma transform information grouped by superblock.
/// </summary>
@ -188,6 +203,102 @@ internal partial class Av1FrameInfo
/// </summary>
public int SuperblockModeInfoSize => this.modeInfoSizePerSuperblock;
/// <summary>
/// Initializes the active frame's contiguous segment map and applies whole-map inheritance when requested.
/// </summary>
/// <param name="frameHeader">The frame header defining active geometry and segmentation update behavior.</param>
/// <param name="primaryReferenceFrameInfo">
/// The retained state selected by the primary reference, or <see langword="null"/> when no primary reference exists.
/// </param>
public void InitializeSegmentIds(ObuFrameHeader frameHeader, Av1FrameInfo? primaryReferenceFrameInfo)
{
ObuSegmentationParameters segmentationParameters = frameHeader.SegmentationParameters;
if (!segmentationParameters.Enabled)
{
// A disabled map is normatively all zero. Empty storage represents that state without retaining one byte
// for every 4x4 position on frames that cannot use segmentation.
return;
}
this.segmentIdColumnCount = frameHeader.ModeInfoColumnCount;
this.segmentIdRowCount = frameHeader.ModeInfoRowCount;
this.segmentIds = new byte[this.segmentIdColumnCount * this.segmentIdRowCount];
if (segmentationParameters.SegmentationUpdateMap == 0 &&
primaryReferenceFrameInfo is not null &&
primaryReferenceFrameInfo.segmentIdColumnCount == this.segmentIdColumnCount &&
primaryReferenceFrameInfo.segmentIdRowCount == this.segmentIdRowCount)
{
// AV1 decodemv.c copies the selected primary frame's block coverage when update_map is zero. Copying the
// same contiguous map once establishes the identical final state without repeating a row copy per block.
primaryReferenceFrameInfo.segmentIds.CopyTo(this.segmentIds, 0);
}
}
/// <summary>
/// Gets the segment identifier stored at one active 4x4 mode-information position.
/// </summary>
/// <param name="row">The zero-based mode-information row.</param>
/// <param name="column">The zero-based mode-information column.</param>
/// <returns>The segment identifier stored at the requested position.</returns>
public byte GetSegmentId(int row, int column) => this.segmentIds[(row * this.segmentIdColumnCount) + column];
/// <summary>
/// Gets the minimum retained segment identifier across a block's clipped mode-information coverage.
/// </summary>
/// <param name="blockSize">The block size whose 4x4 coverage is inspected.</param>
/// <param name="modeInfoPosition">The block origin in frame-relative 4x4 units.</param>
/// <returns>
/// The minimum retained segment identifier, or zero when the retained frame has no enabled segmentation map.
/// </returns>
public int GetPredictedSegmentId(Av1BlockSize blockSize, Point modeInfoPosition)
{
if (this.segmentIds.Length == 0)
{
// libaom represents an unavailable prior map with a null pointer and predicts segment zero.
return 0;
}
int columnCount = Math.Min(blockSize.Get4x4WideCount(), this.segmentIdColumnCount - modeInfoPosition.X);
int rowCount = Math.Min(blockSize.Get4x4HighCount(), this.segmentIdRowCount - modeInfoPosition.Y);
int segmentId = Av1Constants.MaxSegmentCount;
// Temporal prediction uses the minimum over every clipped 4x4 cell, not merely the block origin. This is the
// dec_get_segment_id rule used when segmentation_temporal_update selects the retained primary map.
for (int row = 0; row < rowCount; row++)
{
int offset = ((modeInfoPosition.Y + row) * this.segmentIdColumnCount) + modeInfoPosition.X;
ReadOnlySpan<byte> segmentRow = this.segmentIds.AsSpan(offset, columnCount);
for (int column = 0; column < segmentRow.Length; column++)
{
segmentId = Math.Min(segmentId, segmentRow[column]);
}
}
return segmentId;
}
/// <summary>
/// Writes one segment identifier over a block's clipped mode-information coverage.
/// </summary>
/// <param name="blockSize">The block size whose 4x4 coverage is updated.</param>
/// <param name="modeInfoPosition">The block origin in frame-relative 4x4 units.</param>
/// <param name="segmentId">The decoded segment identifier.</param>
public void SetSegmentId(Av1BlockSize blockSize, Point modeInfoPosition, int segmentId)
{
int columnCount = Math.Min(blockSize.Get4x4WideCount(), this.segmentIdColumnCount - modeInfoPosition.X);
int rowCount = Math.Min(blockSize.Get4x4HighCount(), this.segmentIdRowCount - modeInfoPosition.Y);
// Each block contributes one ID to all covered 4x4 cells. Filling contiguous row slices retains the native
// row-major layout without the per-row object indirection of the previous jagged map.
for (int row = 0; row < rowCount; row++)
{
int offset = ((modeInfoPosition.Y + row) * this.segmentIdColumnCount) + modeInfoPosition.X;
this.segmentIds.AsSpan(offset, columnCount).Fill((byte)segmentId);
}
}
/// <summary>
/// Allocates the loop-restoration unit grid described by the active frame header.
/// </summary>
@ -440,8 +551,10 @@ internal partial class Av1FrameInfo
/// <param name="superblockInfo">The containing superblock.</param>
public void UpdateModeInfo(Av1BlockModeInfo modeInfo, Av1SuperblockInfo superblockInfo)
{
Point modeInfoPosition = this.GetModeInfoPosition(superblockInfo.Position, modeInfo.PositionInSuperblock);
this.modeInfos[this.modeInfoMap.NextIndex] = modeInfo;
this.modeInfoMap.Update(this.GetModeInfoPosition(superblockInfo.Position, modeInfo.PositionInSuperblock), modeInfo.BlockSize);
this.UpdateRetainedMotionField(modeInfo, modeInfoPosition);
this.modeInfoMap.Update(modeInfoPosition, modeInfo.BlockSize);
}
/// <summary>

30
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1InterIntraMode.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// <summary>
/// Identifies the intra predictor blended with a single-reference inter predictor.
/// </summary>
internal enum Av1InterIntraMode : byte
{
/// <summary>
/// Uses a DC intra predictor.
/// </summary>
DC = 0,
/// <summary>
/// Uses a vertical intra predictor.
/// </summary>
Vertical = 1,
/// <summary>
/// Uses a horizontal intra predictor.
/// </summary>
Horizontal = 2,
/// <summary>
/// Uses a smooth intra predictor.
/// </summary>
Smooth = 3,
}

25
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1MotionMode.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// <summary>
/// Identifies the motion model used to construct an AV1 inter predictor.
/// </summary>
internal enum Av1MotionMode : byte
{
/// <summary>
/// Uses translational motion compensation without neighboring-block overlap.
/// </summary>
SimpleTranslation = 0,
/// <summary>
/// Blends the block with predictions derived from overlapping above and left neighbors.
/// </summary>
Obmc = 1,
/// <summary>
/// Uses a locally derived warped-motion model.
/// </summary>
Warped = 2,
}

59
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionInfo.cs

@ -10,10 +10,30 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// <summary>
/// Describes a decoded AV1 partition's block geometry, neighbors, and frame-boundary availability.
/// </summary>
internal class Av1PartitionInfo
internal ref struct Av1PartitionInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="Av1PartitionInfo"/> class.
/// The luma block width in samples.
/// </summary>
private int lumaWidthInPixels;
/// <summary>
/// The shared chroma block width in samples.
/// </summary>
private int chromaWidthInPixels;
/// <summary>
/// The luma block height in samples.
/// </summary>
private int lumaHeightInPixels;
/// <summary>
/// The shared chroma block height in samples.
/// </summary>
private int chromaHeightInPixels;
/// <summary>
/// Initializes a new instance of the <see cref="Av1PartitionInfo"/> structure.
/// </summary>
/// <param name="modeInfo">The decoded mode information for the partition block.</param>
/// <param name="superblockInfo">The containing superblock.</param>
@ -25,9 +45,6 @@ internal class Av1PartitionInfo
this.SuperblockInfo = superblockInfo;
this.IsChroma = isChroma;
this.Type = partitionType;
this.ReferenceFrame = [-1, -1];
this.WidthInPixels = new int[3];
this.HeightInPixels = new int[3];
}
/// <summary>
@ -101,9 +118,9 @@ internal class Av1PartitionInfo
public Av1BlockModeInfo? LeftModeInfoForChroma { get; set; }
/// <summary>
/// Gets or sets the reference-frame identifiers selected for the block.
/// Gets the reference-frame types selected for the block.
/// </summary>
public int[] ReferenceFrame { get; set; }
public Span<Av1ReferenceFrameType> ReferenceFrames => this.ModeInfo.ReferenceFrames;
/// <summary>
/// Gets the signed distance from the block to the left frame edge in one-eighth-sample units.
@ -126,19 +143,23 @@ internal class Av1PartitionInfo
public int ModeBlockToBottomEdge { get; private set; }
/// <summary>
/// Gets the block width in samples for each color plane.
/// Gets or sets the neighboring luma samples used by chroma-from-luma prediction.
/// </summary>
public int[] WidthInPixels { get; private set; }
public Av1ChromaFromLumaContext? ChromaFromLumaContext { get; internal set; }
/// <summary>
/// Gets the block height in samples for each color plane.
/// Gets the block width in samples for a color plane.
/// </summary>
public int[] HeightInPixels { get; private set; }
/// <param name="plane">The luma, blue-difference, or red-difference plane.</param>
/// <returns>The block width in samples for the requested plane.</returns>
public int GetWidthInPixels(Av1Plane plane) => plane == Av1Plane.Y ? this.lumaWidthInPixels : this.chromaWidthInPixels;
/// <summary>
/// Gets or sets the neighboring luma samples used by chroma-from-luma prediction.
/// Gets the block height in samples for a color plane.
/// </summary>
public Av1ChromaFromLumaContext? ChromaFromLumaContext { get; internal set; }
/// <param name="plane">The luma, blue-difference, or red-difference plane.</param>
/// <returns>The block height in samples for the requested plane.</returns>
public int GetHeightInPixels(Av1Plane plane) => plane == Av1Plane.Y ? this.lumaHeightInPixels : this.chromaHeightInPixels;
/// <summary>
/// Computes tile-neighbor availability, frame-edge distances, and per-plane block dimensions.
@ -167,14 +188,10 @@ internal class Av1PartitionInfo
// The bitstream expresses block size on the luma grid. Chroma dimensions are derived by
// subsampling that grid while retaining at least one 4x4 chroma unit for narrow blocks.
const int modeInfoSize = 1 << Av1Constants.ModeInfoSizeLog2;
this.WidthInPixels[0] = bw4 * modeInfoSize;
this.HeightInPixels[0] = bh4 * modeInfoSize;
this.WidthInPixels[1] = Math.Max(1, bw4 >> subX) * modeInfoSize;
this.HeightInPixels[1] = Math.Max(1, bh4 >> subY) * modeInfoSize;
this.WidthInPixels[2] = Math.Max(1, bw4 >> subX) * modeInfoSize;
this.HeightInPixels[2] = Math.Max(1, bh4 >> subY) * modeInfoSize;
this.lumaWidthInPixels = bw4 * modeInfoSize;
this.lumaHeightInPixels = bh4 * modeInfoSize;
this.chromaWidthInPixels = Math.Max(1, bw4 >> subX) * modeInfoSize;
this.chromaHeightInPixels = Math.Max(1, bh4 >> subY) * modeInfoSize;
}
/// <summary>

55
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ReferenceFrameType.cs

@ -0,0 +1,55 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// <summary>
/// Identifies the current or retained frame used to predict an AV1 coding block.
/// </summary>
internal enum Av1ReferenceFrameType : sbyte
{
/// <summary>
/// Indicates that the optional secondary reference is absent.
/// </summary>
None = -1,
/// <summary>
/// References the current frame for intra prediction.
/// </summary>
Intra = 0,
/// <summary>
/// References the most recent forward prediction frame.
/// </summary>
Last = 1,
/// <summary>
/// References the second most recent forward prediction frame.
/// </summary>
Last2 = 2,
/// <summary>
/// References the third most recent forward prediction frame.
/// </summary>
Last3 = 3,
/// <summary>
/// References the long-term golden forward prediction frame.
/// </summary>
Golden = 4,
/// <summary>
/// References the nearest backward prediction frame.
/// </summary>
Backward = 5,
/// <summary>
/// References the secondary alternate backward prediction frame.
/// </summary>
Alternate2 = 6,
/// <summary>
/// References the alternate backward prediction frame.
/// </summary>
Alternate = 7,
}

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

@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -17,40 +18,6 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// </summary>
internal sealed class Av1TileReader : IAv1TileReader, IDisposable
{
/// <summary>
/// The default self-guided restoration projection coefficients for each color plane.
/// </summary>
private static readonly int[] SgrprojXqdMid = [-32, 31];
/// <summary>
/// The default Wiener restoration taps retained between restoration units.
/// </summary>
private static readonly int[] WienerTapsMid = [3, -7, 15];
/// <summary>
/// The minimum transmitted value for each independent Wiener coefficient.
/// </summary>
private static readonly int[] WienerCoefficientMinimum = [-5, -23, -17];
/// <summary>
/// The number of possible transmitted values for each independent Wiener coefficient.
/// </summary>
private static readonly int[] WienerCoefficientValueCount = [16, 32, 64];
/// <summary>
/// The subexponential group-size exponent for each independent Wiener coefficient.
/// </summary>
private static readonly int[] WienerCoefficientSubexponentialK = [1, 2, 3];
/// <summary>
/// The two self-guided filter radii selected by each parameter-set index.
/// </summary>
private static readonly int[][] SgrProjectionRadii =
[
[2, 1], [2, 1], [2, 1], [2, 1], [2, 1], [2, 1], [2, 1], [2, 1],
[2, 1], [2, 1], [0, 1], [0, 1], [0, 1], [0, 1], [2, 0], [2, 0]
];
/// <summary>
/// The minimum value of the first self-guided projection coefficient.
/// </summary>
@ -72,31 +39,25 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
private const int SgrProjectionSubexponentialK = 4;
/// <summary>
/// Maps packed coefficient sign classes to their signed contribution to the DC context.
/// </summary>
private static readonly int[] Signs = [0, -1, 1];
/// <summary>
/// Maps the summed neighboring DC signs to the AV1 DC-sign entropy context.
/// </summary>
private static readonly int[] DcSignContexts = [
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2];
/// <summary>
/// Maps the weighted palette-neighbor score hash to its color-index entropy context.
/// The two self-guided filter radii selected by each parameter-set index.
/// </summary>
private static readonly int[] PaletteColorIndexContexts = [-1, -1, 0, -1, -1, 4, 3, 2, 1];
private static readonly int[][] SgrProjectionRadii =
[
[2, 1], [2, 1], [2, 1], [2, 1], [2, 1], [2, 1], [2, 1], [2, 1],
[2, 1], [2, 1], [0, 1], [0, 1], [0, 1], [0, 1], [2, 0], [2, 0]
];
/// <summary>
/// Stores the preceding self-guided restoration coefficients for each color plane.
/// Stores two preceding self-guided restoration coefficients for each of the three color planes, indexed by
/// <c>(plane * 2) + coefficient</c>.
/// </summary>
private int[][] referenceSgrXqd = [];
private InlineArray6<int> referenceSgrXqd;
/// <summary>
/// Stores the preceding vertical and horizontal Wiener taps for each color plane.
/// Stores three preceding Wiener taps for both passes of each of the three color planes, indexed by
/// <c>(((plane * 2) + pass) * 3) + tap</c>.
/// </summary>
private int[][][] referenceLrWiener = [];
private InlineArray18<int> referenceLrWiener;
/// <summary>
/// Tracks entropy, partition, and transform state above the current block.
@ -118,11 +79,6 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// </summary>
private readonly int[] currentDeltaLoopFilter = new int[Av1Constants.FrameLoopFilterCount];
/// <summary>
/// Stores the segment identifier covering each 4x4 frame position.
/// </summary>
private readonly int[][] segmentIds = [];
/// <summary>
/// Stores per-plane transform counts for each forced 64x64 residual region.
/// </summary>
@ -158,6 +114,16 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// </summary>
private readonly IAv1FrameDecoder? frameDecoder;
/// <summary>
/// The decoder-session entropy contexts reused by every tile in the current frame.
/// </summary>
private readonly Av1FrameEntropyContexts entropyContexts;
/// <summary>
/// The retained primary frame whose segment map supplies temporal segment-ID predictions.
/// </summary>
private readonly Av1FrameInfo? primaryReferenceFrameInfo;
/// <summary>
/// Initializes a new instance of the <see cref="Av1TileReader"/> class for syntax parsing without reconstruction.
/// </summary>
@ -165,20 +131,57 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="sequenceHeader">The active AV1 sequence header.</param>
/// <param name="frameHeader">The frame header whose tiles will be parsed.</param>
public Av1TileReader(Configuration configuration, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader)
: this(configuration, sequenceHeader, frameHeader, new(frameHeader.QuantizationParameters.BaseQIndex), null, null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1TileReader"/> class with decoder-session entropy state.
/// </summary>
/// <param name="configuration">The decoder configuration.</param>
/// <param name="sequenceHeader">The active AV1 sequence header.</param>
/// <param name="frameHeader">The frame header whose tiles will be parsed.</param>
/// <param name="entropyContexts">The entropy contexts reused by the owning decoder session.</param>
/// <param name="primaryReferenceContext">
/// The retained primary-reference entropy context, or <see langword="null"/> when the frame selects defaults.
/// </param>
/// <param name="referenceFrames">
/// The retained reconstructed frames, or <see langword="null"/> for the intra-only syntax reader.
/// </param>
public Av1TileReader(
Configuration configuration,
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader,
Av1FrameEntropyContexts entropyContexts,
Av1FrameEntropyContext? primaryReferenceContext,
Av1ReferenceFrameStore? referenceFrames)
{
this.FrameHeader = frameHeader;
this.configuration = configuration;
this.SequenceHeader = sequenceHeader;
this.entropyContexts = entropyContexts;
this.entropyContexts.BeginFrame(frameHeader.QuantizationParameters.BaseQIndex, primaryReferenceContext);
// FrameInfo owns all traversal-order records and coefficient storage produced by the tile readers.
this.FrameInfo = new(this.SequenceHeader);
this.FrameInfo.InitializeLoopRestoration(this.SequenceHeader, this.FrameHeader);
this.segmentIds = new int[this.FrameHeader.ModeInfoRowCount][];
for (int y = 0; y < this.FrameHeader.ModeInfoRowCount; y++)
if (referenceFrames is not null)
{
this.segmentIds[y] = new int[this.FrameHeader.ModeInfoColumnCount];
// Only the production decoder owns reconstructed references. Header-only intra readers retain their
// existing allocation profile and cannot reach inter mode parsing.
this.FrameInfo.InitializeMotionField(this.SequenceHeader, this.FrameHeader, referenceFrames);
byte? primaryReferenceSlot = this.FrameHeader.PrimaryReferenceSlot;
if (primaryReferenceSlot is not null)
{
// The uncompressed-header parser has already validated this slot. Keep only its frame-state owner;
// segment samples remain in the retained frame and are copied only for whole-map inheritance.
this.primaryReferenceFrameInfo = referenceFrames.Resolve(primaryReferenceSlot.Value)!.FrameInfo;
}
}
this.FrameInfo.InitializeSegmentIds(this.FrameHeader, this.primaryReferenceFrameInfo);
this.FrameInfo.InitializeLoopRestoration(this.SequenceHeader, this.FrameHeader);
// Above contexts span the aligned frame width, while left contexts are reused for each superblock row.
int planesCount = sequenceHeader.ColorConfig.PlaneCount;
int superblockColumnCount =
@ -216,6 +219,48 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
: this(configuration, sequenceHeader, frameHeader)
=> this.frameDecoder = frameDecoder;
/// <summary>
/// Gets the default self-guided restoration projection coefficients for each color plane.
/// </summary>
private static ReadOnlySpan<int> SgrprojXqdMid => [-32, 31];
/// <summary>
/// Gets the default Wiener restoration taps retained between restoration units.
/// </summary>
private static ReadOnlySpan<int> WienerTapsMid => [3, -7, 15];
/// <summary>
/// Gets the minimum transmitted value for each independent Wiener coefficient.
/// </summary>
private static ReadOnlySpan<int> WienerCoefficientMinimum => [-5, -23, -17];
/// <summary>
/// Gets the number of possible transmitted values for each independent Wiener coefficient.
/// </summary>
private static ReadOnlySpan<int> WienerCoefficientValueCount => [16, 32, 64];
/// <summary>
/// Gets the subexponential group-size exponent for each independent Wiener coefficient.
/// </summary>
private static ReadOnlySpan<int> WienerCoefficientSubexponentialK => [1, 2, 3];
/// <summary>
/// Gets the signed DC-context contribution for each packed coefficient sign class.
/// </summary>
private static ReadOnlySpan<int> Signs => [0, -1, 1];
/// <summary>
/// Gets the AV1 DC-sign entropy context for each summed neighboring sign value.
/// </summary>
private static ReadOnlySpan<int> DcSignContexts => [
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2];
/// <summary>
/// Gets the color-index entropy context for each weighted palette-neighbor score hash.
/// </summary>
private static ReadOnlySpan<int> PaletteColorIndexContexts => [-1, -1, 0, -1, -1, 4, 3, 2, 1];
/// <summary>
/// Gets the frame header whose tile syntax is being parsed.
/// </summary>
@ -227,10 +272,20 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
public ObuSequenceHeader SequenceHeader { get; }
/// <summary>
/// Gets the frame-owned mode, transform, coefficient, quantizer, and filter state populated by tile parsing.
/// Gets the frame-owned mode, motion, transform, coefficient, quantizer, and filter state populated by tile parsing.
/// </summary>
public Av1FrameInfo FrameInfo { get; }
/// <summary>
/// Gets the completed frame entropy context selected by the context-update tile.
/// </summary>
/// <remarks>
/// The context contains either normative defaults or the selected primary-reference state until the signaled
/// update tile has decoded successfully. Callers that retain it beyond this reader's frame lifecycle must copy it
/// through <see cref="Av1FrameEntropyContext.SnapshotTo"/>.
/// </remarks>
public Av1FrameEntropyContext FrameEntropyContext => this.entropyContexts.Published;
/// <summary>
/// Returns the tile-neighbor context storage to the configured memory allocator.
/// </summary>
@ -248,12 +303,17 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <remarks>Corresponds to <c>parse_tile</c> in SVT-AV1.</remarks>
public void ReadTile(Span<byte> tileData, int tileNum)
{
// AV1 tiles never inherit adaptation from another tile in the same frame. Reusing one graph is safe because
// parsing is sequential and every entry is restored from the unchanged frame base before the range decoder is
// constructed.
this.entropyContexts.Working.CopyFrom(this.entropyContexts.Base);
// The frame syntax exposes a disable flag, while the range reader follows libaom's positive
// allow_update_cdf convention.
Av1SymbolDecoder reader = new(
this.configuration,
tileData,
this.FrameHeader.QuantizationParameters.BaseQIndex,
this.entropyContexts.Working,
!this.FrameHeader.DisableCdfUpdate);
int tileColumnIndex = tileNum % this.FrameHeader.TilesInfo.TileColumnCount;
@ -268,18 +328,18 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
this.ClearLoopFilterDelta();
int planesCount = this.SequenceHeader.ColorConfig.PlaneCount;
// Restoration coefficients are differentially coded, so each tile begins from the AV1 defaults.
this.referenceSgrXqd = new int[planesCount][];
this.referenceLrWiener = new int[planesCount][][];
// AV1 fixes restoration reference storage at three planes, two directions or projection coefficients, and
// three transmitted Wiener taps. Populate the inline value storage in place so every tile starts from the
// normative differential-coding defaults without constructing jagged arrays.
Span<int> sgrReferences = this.referenceSgrXqd;
Span<int> wienerReferences = this.referenceLrWiener;
for (int plane = 0; plane < planesCount; plane++)
{
this.referenceSgrXqd[plane] = new int[2];
Array.Copy(SgrprojXqdMid, this.referenceSgrXqd[plane], SgrprojXqdMid.Length);
this.referenceLrWiener[plane] = new int[2][];
SgrprojXqdMid.CopyTo(sgrReferences.Slice(plane * 2, 2));
for (int pass = 0; pass < 2; pass++)
{
this.referenceLrWiener[plane][pass] = new int[Av1Constants.WienerCoefficientCount];
Array.Copy(WienerTapsMid, this.referenceLrWiener[plane][pass], WienerTapsMid.Length);
int referenceOffset = ((plane * 2) + pass) * Av1Constants.WienerCoefficientCount;
WienerTapsMid.CopyTo(wienerReferences.Slice(referenceOffset, Av1Constants.WienerCoefficientCount));
}
}
@ -309,6 +369,29 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
this.frameDecoder?.DecodeSuperblock(modeInfoPosition, superblockInfo, tileInfo);
}
}
// Range decoding may read implicit zero padding while normalizing its final interval. Validate the logical
// stopping position before publishing either pixels or adapted CDF state so a truncated tile cannot commit.
reader.ValidateTrailingBits();
if (!this.FrameHeader.DisableFrameEndUpdateCdf && tileNum == this.FrameHeader.TilesInfo.ContextUpdateTileId)
{
// libaom publishes only context_update_tile_id after every tile has independently started from the frame
// base, then clears its CDF counters. Snapshotting into a third reusable graph preserves the unchanged base
// for tiles that follow the selected tile in bitstream order.
this.entropyContexts.Working.SnapshotTo(this.entropyContexts.Published);
}
}
/// <summary>
/// Completes the current coded frame.
/// </summary>
public void CompleteFrame()
{
// Tile parsing and optional incremental superblock reconstruction finish inside ReadTile. The owning AV1
// decoder uses this lifecycle boundary to assemble native planes. A directly created tile reader owns its
// neighbor-context rents, so the same boundary must return them before ObuReader releases the frame instance.
this.Dispose();
}
/// <summary>
@ -425,15 +508,16 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
destination[0] = 0;
for (int coefficient = firstCoefficient; coefficient < Av1Constants.WienerCoefficientCount; coefficient++)
{
int referenceIndex = (((plane * 2) + pass) * Av1Constants.WienerCoefficientCount) + coefficient;
int minimum = WienerCoefficientMinimum[coefficient];
int value = reader.ReadReferenceSubexponential(
WienerCoefficientValueCount[coefficient],
WienerCoefficientSubexponentialK[coefficient],
this.referenceLrWiener[plane][pass][coefficient] - minimum);
this.referenceLrWiener[referenceIndex] - minimum);
value += minimum;
destination[coefficient] = value;
this.referenceLrWiener[plane][pass][coefficient] = value;
this.referenceLrWiener[referenceIndex] = value;
}
}
}
@ -449,7 +533,8 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
unit.SgrParameterSet = reader.ReadLiteral(4);
int[] radii = SgrProjectionRadii[unit.SgrParameterSet];
int[] coefficients = unit.SgrProjectionCoefficients;
int[] references = this.referenceSgrXqd[plane];
Span<int> allReferences = this.referenceSgrXqd;
Span<int> references = allReferences.Slice(plane * 2, 2);
if (radii[0] == 0)
{
coefficients[0] = 0;
@ -472,7 +557,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
coefficients[1] = ReadSgrProjectionCoefficient(ref reader, references[1], SgrProjectionCoefficient1Minimum);
}
coefficients.CopyTo(references, 0);
coefficients.CopyTo(references);
}
/// <summary>
@ -681,15 +766,15 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
partitionInfo.PopulateModeInfoNeighbors(this.SequenceHeader.ColorConfig);
this.ReadModeInfo(ref reader, partitionInfo, tileInfo);
this.ReadPaletteTokens(ref reader, partitionInfo);
this.ReadBlockTransformSize(ref reader, modeInfoLocation, partitionInfo, superblockInfo, tileInfo);
this.ReadModeInfo(ref reader, ref partitionInfo, tileInfo);
this.ReadPaletteTokens(ref reader, ref partitionInfo);
this.ReadBlockTransformSize(ref reader, modeInfoLocation, ref partitionInfo, superblockInfo, tileInfo);
if (partitionInfo.ModeInfo.Skip)
{
this.ResetSkipContext(partitionInfo, tileInfo);
this.ResetSkipContext(ref partitionInfo, tileInfo);
}
this.Residual(ref reader, partitionInfo, superblockInfo, tileInfo, blockSize);
this.Residual(ref reader, ref partitionInfo, superblockInfo, tileInfo, blockSize);
// Store the record only after all syntax has populated it, then map every covered 4x4 position.
this.FrameInfo.UpdateModeInfo(blockModeInfo, superblockInfo);
@ -701,7 +786,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="partitionInfo">The skipped block and its frame position.</param>
/// <param name="tileInfo">The active tile boundaries.</param>
/// <remarks>Corresponds to <c>reset_skip_context</c> in SVT-AV1.</remarks>
private void ResetSkipContext(Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo)
private void ResetSkipContext(ref Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo)
{
int planesCount = this.SequenceHeader.ColorConfig.PlaneCount;
for (int i = 0; i < planesCount; i++)
@ -728,7 +813,12 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="tileInfo">The active tile boundaries.</param>
/// <param name="blockSize">The coding block size.</param>
/// <remarks>Implements AV1 section 5.11.34 and corresponds to <c>parse_residual</c> in SVT-AV1.</remarks>
private void Residual(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo, Av1BlockSize blockSize)
private void Residual(
ref Av1SymbolDecoder reader,
ref Av1PartitionInfo partitionInfo,
Av1SuperblockInfo superblockInfo,
Av1TileInfo tileInfo,
Av1BlockSize blockSize)
{
int maxBlocksWide = partitionInfo.GetMaxBlockWide(blockSize, false);
int maxBlocksHigh = partitionInfo.GetMaxBlockHigh(blockSize, false);
@ -823,7 +913,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
Span<int> coefficientBuffer = superblockInfo.GetCoefficients((Av1Plane)plane)[coefficientIndex..];
endOfBlock = this.ParseTransformBlock(
ref reader,
partitionInfo,
ref partitionInfo,
tileInfo,
coefficientBuffer,
transformInfo,
@ -913,7 +1003,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// </remarks>
private int ParseTransformBlock(
ref Av1SymbolDecoder reader,
Av1PartitionInfo partitionInfo,
ref Av1PartitionInfo partitionInfo,
Av1TileInfo tileInfo,
Span<int> coefficientBuffer,
Av1TransformInfo transformInfo,
@ -960,7 +1050,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
endOfBlock = this.ParseCoefficients(
ref reader,
partitionInfo,
ref partitionInfo,
blockRow,
blockColumn,
aboveContextOffset,
@ -992,7 +1082,18 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <remarks>
/// Implements AV1 section 5.11.39 using the traversal shape of the corresponding SVT-AV1 implementation.
/// </remarks>
private int ParseCoefficients(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo, int blockRow, int blockColumn, int aboveOffset, int leftOffset, int plane, Av1TransformBlockContext transformBlockContext, Av1TransformSize transformSize, Av1TransformInfo transformInfo, Span<int> coefficientBuffer)
private int ParseCoefficients(
ref Av1SymbolDecoder reader,
ref Av1PartitionInfo partitionInfo,
int blockRow,
int blockColumn,
int aboveOffset,
int leftOffset,
int plane,
Av1TransformBlockContext transformBlockContext,
Av1TransformSize transformSize,
Av1TransformInfo transformInfo,
Span<int> coefficientBuffer)
{
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
@ -1063,7 +1164,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
{
uint sign = (uint)aboveContext[k] >> Av1Constants.CoefficientContextBitCount;
DebugGuard.MustBeLessThanOrEqualTo(sign, 2U, nameof(sign));
dcSign += Signs[sign];
dcSign += Signs[(int)sign];
}
while (++k < transformBlockUnitWideCount);
@ -1072,7 +1173,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
{
uint sign = (uint)leftContext[k] >> Av1Constants.CoefficientContextBitCount;
DebugGuard.MustBeLessThanOrEqualTo(sign, 2U, nameof(sign));
dcSign += Signs[sign];
dcSign += Signs[(int)sign];
}
while (++k < transformBlockUnitHighCount);
@ -1168,7 +1269,12 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="allowSelect">A value indicating whether transform-size selection syntax is allowed at this node.</param>
/// <returns>The selected transform size.</returns>
/// <remarks>Implements AV1 section 5.11.15.</remarks>
private Av1TransformSize ReadTransformSize(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo, bool allowSelect)
private Av1TransformSize ReadTransformSize(
ref Av1SymbolDecoder reader,
ref Av1PartitionInfo partitionInfo,
Av1SuperblockInfo superblockInfo,
Av1TileInfo tileInfo,
bool allowSelect)
{
Av1BlockModeInfo modeInfo = partitionInfo.ModeInfo;
if (this.FrameHeader.LosslessArray[modeInfo.SegmentId])
@ -1178,7 +1284,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
if (modeInfo.BlockSize > Av1BlockSize.Block4x4 && allowSelect && this.FrameHeader.TransformMode == Av1TransformMode.Select)
{
return this.ReadSelectedTransformSize(ref reader, partitionInfo, superblockInfo, tileInfo);
return this.ReadSelectedTransformSize(ref reader, ref partitionInfo, superblockInfo, tileInfo);
}
return modeInfo.BlockSize.GetMaximumTransformSize();
@ -1192,7 +1298,11 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="superblockInfo">The containing superblock.</param>
/// <param name="tileInfo">The active tile boundaries.</param>
/// <returns>The decoded transform size.</returns>
private Av1TransformSize ReadSelectedTransformSize(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo)
private Av1TransformSize ReadSelectedTransformSize(
ref Av1SymbolDecoder reader,
ref Av1PartitionInfo partitionInfo,
Av1SuperblockInfo superblockInfo,
Av1TileInfo tileInfo)
{
int context = 0;
Av1TransformSize maxTransformSize = partitionInfo.ModeInfo.BlockSize.GetMaximumTransformSize();
@ -1232,17 +1342,22 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="superblockInfo">The containing superblock.</param>
/// <param name="tileInfo">The active tile boundaries.</param>
/// <remarks>Implements AV1 section 5.11.16 and corresponds to <c>read_block_tx_size</c> in SVT-AV1.</remarks>
private void ReadBlockTransformSize(ref Av1SymbolDecoder reader, Point modeInfoLocation, Av1PartitionInfo partitionInfo, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo)
private void ReadBlockTransformSize(
ref Av1SymbolDecoder reader,
Point modeInfoLocation,
ref Av1PartitionInfo partitionInfo,
Av1SuperblockInfo superblockInfo,
Av1TileInfo tileInfo)
{
Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize;
int block4x4Width = blockSize.Get4x4WideCount();
int block4x4Height = blockSize.Get4x4HighCount();
// HEIF still-image decoding follows the independently decodable intra-frame transform-size branch.
Av1TransformSize transformSize = this.ReadTransformSize(ref reader, partitionInfo, superblockInfo, tileInfo, true);
Av1TransformSize transformSize = this.ReadTransformSize(ref reader, ref partitionInfo, superblockInfo, tileInfo, true);
this.aboveNeighborContext.UpdateTransformation(modeInfoLocation, tileInfo, transformSize, blockSize, false);
this.leftNeighborContext.UpdateTransformation(modeInfoLocation, superblockInfo, transformSize, blockSize, false);
this.UpdateTransformInfo(partitionInfo, superblockInfo, blockSize, transformSize);
this.UpdateTransformInfo(ref partitionInfo, superblockInfo, blockSize, transformSize);
}
/// <summary>
@ -1252,7 +1367,11 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="superblockInfo">The containing superblock and transform storage.</param>
/// <param name="blockSize">The coding block size.</param>
/// <param name="transformSize">The selected luma transform size.</param>
private unsafe void UpdateTransformInfo(Av1PartitionInfo partitionInfo, Av1SuperblockInfo superblockInfo, Av1BlockSize blockSize, Av1TransformSize transformSize)
private unsafe void UpdateTransformInfo(
ref Av1PartitionInfo partitionInfo,
Av1SuperblockInfo superblockInfo,
Av1BlockSize blockSize,
Av1TransformSize transformSize)
{
int transformInfoYIndex = partitionInfo.ModeInfo.GetFirstTransformLocation(Av1PlaneType.Y);
int transformInfoUvIndex = partitionInfo.ModeInfo.GetFirstTransformLocation(Av1PlaneType.Uv);
@ -1367,13 +1486,13 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="reader">The tile symbol decoder.</param>
/// <param name="partitionInfo">The current coding block.</param>
/// <remarks>Implements AV1 section 5.11.49.</remarks>
private void ReadPaletteTokens(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo)
private void ReadPaletteTokens(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo)
{
Av1BlockModeInfo modeInfo = partitionInfo.ModeInfo;
if (modeInfo.GetPaletteSize(Av1PlaneType.Y) != 0)
{
GetPaletteMapDimensions(
partitionInfo,
ref partitionInfo,
Av1PlaneType.Y,
this.SequenceHeader.ColorConfig,
out int planeWidth,
@ -1396,7 +1515,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
if (modeInfo.GetPaletteSize(Av1PlaneType.Uv) != 0)
{
GetPaletteMapDimensions(
partitionInfo,
ref partitionInfo,
Av1PlaneType.Uv,
this.SequenceHeader.ColorConfig,
out int planeWidth,
@ -1424,10 +1543,10 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <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>
private void ReadModeInfo(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo)
private void ReadModeInfo(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo)
{
DebugGuard.IsTrue(this.FrameHeader.FrameType is ObuFrameType.KeyFrame or ObuFrameType.IntraOnlyFrame, "Only INTRA frames supported.");
this.ReadIntraFrameModeInfo(ref reader, partitionInfo, tileInfo);
this.ReadIntraFrameModeInfo(ref reader, ref partitionInfo, tileInfo);
}
/// <summary>
@ -1437,30 +1556,30 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <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>
private void ReadIntraFrameModeInfo(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo)
private void ReadIntraFrameModeInfo(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo)
{
if (this.FrameHeader.SegmentationParameters.SegmentIdPrecedesSkip)
{
this.IntraSegmentId(ref reader, partitionInfo);
this.IntraSegmentId(ref reader, ref partitionInfo);
}
partitionInfo.ModeInfo.Skip = this.ReadSkip(ref reader, partitionInfo);
partitionInfo.ModeInfo.Skip = this.ReadSkip(ref reader, ref partitionInfo);
if (!this.FrameHeader.SegmentationParameters.SegmentIdPrecedesSkip)
{
this.IntraSegmentId(ref reader, partitionInfo);
this.IntraSegmentId(ref reader, ref partitionInfo);
}
this.ReadCdef(ref reader, partitionInfo);
this.ReadCdef(ref reader, ref partitionInfo);
if (this.FrameHeader.DeltaQParameters.IsPresent)
{
this.ReadDeltaQuantizerIndex(ref reader, partitionInfo);
this.ReadDeltaLoopFilter(ref reader, partitionInfo);
this.ReadDeltaQuantizerIndex(ref reader, ref partitionInfo);
this.ReadDeltaLoopFilter(ref reader, ref partitionInfo);
}
// Independently decodable still-image blocks reference only the current intra frame.
partitionInfo.ReferenceFrame[0] = 0;
partitionInfo.ReferenceFrame[1] = -1;
partitionInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Intra;
partitionInfo.ReferenceFrames[1] = Av1ReferenceFrameType.None;
partitionInfo.ModeInfo.SetPaletteSizes(0, 0);
bool useIntraBlockCopy = false;
if (this.AllowIntraBlockCopy())
@ -1472,17 +1591,17 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
{
partitionInfo.ModeInfo.UseIntraBlockCopy = true;
partitionInfo.ModeInfo.YMode = Av1PredictionMode.DC;
partitionInfo.ModeInfo.UvMode = Av1PredictionMode.DC;
partitionInfo.ModeInfo.UvMode = Av1ChromaPredictionMode.DC;
Av1MotionVector reference = Av1IntraBlockCopy.FindReference(
partitionInfo,
ref partitionInfo,
tileInfo,
this.SequenceHeader.SuperblockModeInfoSize,
this.displacementVectorCandidates,
this.displacementVectorWeights);
Av1MotionVector displacement = reader.ReadDisplacementVector(reference);
if (!Av1IntraBlockCopy.IsValid(displacement, partitionInfo, tileInfo, this.SequenceHeader))
if (!Av1IntraBlockCopy.IsValid(displacement, ref partitionInfo, tileInfo, this.SequenceHeader))
{
throw new InvalidImageContentException("Invalid AV1 intra-block-copy displacement vector.");
}
@ -1499,19 +1618,22 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
if (partitionInfo.IsChroma && !this.SequenceHeader.ColorConfig.IsMonochrome)
{
partitionInfo.ModeInfo.UvMode = reader.ReadIntraModeUv(partitionInfo.ModeInfo.YMode, this.IsChromaForLumaAllowed(partitionInfo));
if (partitionInfo.ModeInfo.UvMode == Av1PredictionMode.UvChromaFromLuma)
partitionInfo.ModeInfo.UvMode = reader.ReadIntraModeUv(
partitionInfo.ModeInfo.YMode,
this.IsChromaForLumaAllowed(ref partitionInfo));
if (partitionInfo.ModeInfo.UvMode == Av1ChromaPredictionMode.ChromaFromLuma)
{
ReadChromaFromLumaAlphas(ref reader, partitionInfo.ModeInfo);
}
partitionInfo.ModeInfo.SetAngleDelta(
Av1PlaneType.Uv,
IntraAngleInfo(ref reader, partitionInfo.ModeInfo.UvMode, partitionInfo.ModeInfo.BlockSize));
IntraAngleInfo(ref reader, partitionInfo.ModeInfo.UvMode.ToLumaMode(), partitionInfo.ModeInfo.BlockSize));
}
else
{
partitionInfo.ModeInfo.UvMode = Av1PredictionMode.DC;
partitionInfo.ModeInfo.UvMode = Av1ChromaPredictionMode.DC;
}
if (partitionInfo.ModeInfo.BlockSize >= Av1BlockSize.Block8x8 &&
@ -1519,10 +1641,10 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
partitionInfo.ModeInfo.BlockSize.GetHeight() <= 64 &&
this.FrameHeader.AllowScreenContentTools)
{
this.PaletteModeInfo(ref reader, partitionInfo);
this.PaletteModeInfo(ref reader, ref partitionInfo);
}
this.FilterIntraModeInfo(ref reader, partitionInfo);
this.FilterIntraModeInfo(ref reader, ref partitionInfo);
}
}
@ -1540,7 +1662,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// </summary>
/// <param name="partitionInfo">The current coding block.</param>
/// <returns><see langword="true"/> when the lossless transform or block dimensions permit chroma-from-luma prediction; otherwise, <see langword="false"/>.</returns>
private bool IsChromaForLumaAllowed(Av1PartitionInfo partitionInfo)
private bool IsChromaForLumaAllowed(ref Av1PartitionInfo partitionInfo)
{
if (this.FrameHeader.LosslessArray[partitionInfo.ModeInfo.SegmentId])
{
@ -1561,7 +1683,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// </summary>
/// <param name="reader">The tile symbol decoder.</param>
/// <param name="partitionInfo">The current coding block.</param>
private void FilterIntraModeInfo(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo)
private void FilterIntraModeInfo(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo)
{
partitionInfo.ModeInfo.UseFilterIntra = false;
if (this.SequenceHeader.EnableFilterIntra &&
@ -1584,7 +1706,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="reader">The tile symbol decoder.</param>
/// <param name="partitionInfo">The current coding block.</param>
/// <remarks>Implements AV1 section 5.11.46.</remarks>
private void PaletteModeInfo(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo)
private void PaletteModeInfo(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo)
{
Av1BlockModeInfo modeInfo = partitionInfo.ModeInfo;
Av1BlockSize blockSize = modeInfo.BlockSize;
@ -1611,20 +1733,20 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
{
yPaletteSize = reader.ReadPaletteSize(blockSizeContext, Av1PlaneType.Y);
Span<ushort> yColors = stackalloc ushort[Av1Constants.PaletteMaxSize];
ReadPaletteColorsY(ref reader, partitionInfo, yPaletteSize, bitDepth, yColors);
ReadPaletteColorsY(ref reader, ref partitionInfo, yPaletteSize, bitDepth, yColors);
modeInfo.SetPaletteColors(Av1Plane.Y, yColors[..yPaletteSize]);
}
}
if (this.SequenceHeader.ColorConfig.PlaneCount > 1 &&
modeInfo.UvMode == Av1PredictionMode.DC &&
modeInfo.UvMode == Av1ChromaPredictionMode.DC &&
partitionInfo.IsChroma &&
reader.ReadPaletteUvMode(yPaletteSize != 0))
{
uvPaletteSize = reader.ReadPaletteSize(blockSizeContext, Av1PlaneType.Uv);
Span<ushort> uColors = stackalloc ushort[Av1Constants.PaletteMaxSize];
Span<ushort> vColors = stackalloc ushort[Av1Constants.PaletteMaxSize];
ReadPaletteColorsUv(ref reader, partitionInfo, uvPaletteSize, bitDepth, uColors, vColors);
ReadPaletteColorsUv(ref reader, ref partitionInfo, uvPaletteSize, bitDepth, uColors, vColors);
modeInfo.SetPaletteColors(Av1Plane.U, uColors[..uvPaletteSize]);
modeInfo.SetPaletteColors(Av1Plane.V, vColors[..uvPaletteSize]);
}
@ -1642,14 +1764,14 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="colors">The destination palette-color buffer.</param>
private static void ReadPaletteColorsY(
ref Av1SymbolDecoder reader,
Av1PartitionInfo partitionInfo,
ref Av1PartitionInfo partitionInfo,
int paletteSize,
int bitDepth,
scoped Span<ushort> colors)
{
Span<ushort> colorCache = stackalloc ushort[Av1Constants.PaletteMaxSize * 2];
Span<ushort> cachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize];
int cacheSize = GetPaletteCache(partitionInfo, Av1Plane.Y, colorCache);
int cacheSize = GetPaletteCache(ref partitionInfo, Av1Plane.Y, colorCache);
int colorIndex = 0;
for (int i = 0; i < cacheSize && colorIndex < paletteSize; i++)
{
@ -1695,7 +1817,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="vColors">The destination V palette-color buffer.</param>
private static void ReadPaletteColorsUv(
ref Av1SymbolDecoder reader,
Av1PartitionInfo partitionInfo,
ref Av1PartitionInfo partitionInfo,
int paletteSize,
int bitDepth,
scoped Span<ushort> uColors,
@ -1703,7 +1825,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
{
Span<ushort> colorCache = stackalloc ushort[Av1Constants.PaletteMaxSize * 2];
Span<ushort> cachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize];
int cacheSize = GetPaletteCache(partitionInfo, Av1Plane.U, colorCache);
int cacheSize = GetPaletteCache(ref partitionInfo, Av1Plane.U, colorCache);
int colorIndex = 0;
for (int i = 0; i < cacheSize && colorIndex < paletteSize; i++)
{
@ -1782,7 +1904,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="plane">The luma or U plane whose sorted base colors form the cache.</param>
/// <param name="cache">The destination cache, which can hold both neighboring palettes.</param>
/// <returns>The number of colors written to <paramref name="cache"/>.</returns>
private static int GetPaletteCache(Av1PartitionInfo partitionInfo, Av1Plane plane, Span<ushort> cache)
private static int GetPaletteCache(ref Av1PartitionInfo partitionInfo, Av1Plane plane, scoped Span<ushort> cache)
{
// AV1 deliberately excludes the block above at a 64-by-64 superblock-row boundary.
int minimumSuperblockHeight = Av1BlockSize.Block64x64.Get4x4HighCount();
@ -1886,7 +2008,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="rows">The number of plane-block rows inside the coded image.</param>
/// <param name="columns">The number of plane-block columns inside the coded image.</param>
private static void GetPaletteMapDimensions(
Av1PartitionInfo partitionInfo,
ref Av1PartitionInfo partitionInfo,
Av1PlaneType planeType,
ObuColorConfig colorConfig,
out int planeWidth,
@ -2103,28 +2225,17 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="reader">The tile symbol decoder.</param>
/// <param name="partitionInfo">The current coding block.</param>
/// <remarks>Implements AV1 section 5.11.8.</remarks>
private void IntraSegmentId(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo)
private void IntraSegmentId(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo)
{
if (this.FrameHeader.SegmentationParameters.Enabled)
if (!this.FrameHeader.SegmentationParameters.Enabled)
{
this.ReadSegmentId(ref reader, partitionInfo);
// Disabled segmentation assigns the default ID without allocating or populating a retained map.
return;
}
int blockWidth4x4 = partitionInfo.ModeInfo.BlockSize.Get4x4WideCount();
int blockHeight4x4 = partitionInfo.ModeInfo.BlockSize.Get4x4HighCount();
int modeInfoCountX = Math.Min(this.FrameHeader.ModeInfoColumnCount - partitionInfo.ColumnIndex, blockWidth4x4);
int modeInfoCountY = Math.Min(this.FrameHeader.ModeInfoRowCount - partitionInfo.RowIndex, blockHeight4x4);
int segmentId = partitionInfo.ModeInfo.SegmentId;
// Later blocks predict from 4x4 positions, so replicate one block ID over its clipped frame coverage.
for (int y = 0; y < modeInfoCountY; y++)
{
int[] segmentRow = this.segmentIds[partitionInfo.RowIndex + y];
for (int x = 0; x < modeInfoCountX; x++)
{
segmentRow[partitionInfo.ColumnIndex + x] = segmentId;
}
}
this.ReadSegmentId(ref reader, ref partitionInfo);
Point modeInfoPosition = new(partitionInfo.ColumnIndex, partitionInfo.RowIndex);
this.FrameInfo.SetSegmentId(partitionInfo.ModeInfo.BlockSize, modeInfoPosition, partitionInfo.ModeInfo.SegmentId);
}
/// <summary>
@ -2133,7 +2244,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="reader">The tile symbol decoder.</param>
/// <param name="partitionInfo">The current coding block and its available neighbors.</param>
/// <remarks>Implements AV1 section 5.11.9.</remarks>
private void ReadSegmentId(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo)
private void ReadSegmentId(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo)
{
int predictor;
int prevUL = -1;
@ -2143,17 +2254,17 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
int rowIndex = partitionInfo.RowIndex;
if (partitionInfo.AvailableAbove && partitionInfo.AvailableLeft)
{
prevUL = Av1SymbolContextHelper.GetSegmentId(this.segmentIds, rowIndex - 1, columnIndex - 1);
prevUL = this.FrameInfo.GetSegmentId(rowIndex - 1, columnIndex - 1);
}
if (partitionInfo.AvailableAbove)
{
prevU = Av1SymbolContextHelper.GetSegmentId(this.segmentIds, rowIndex - 1, columnIndex);
prevU = this.FrameInfo.GetSegmentId(rowIndex - 1, columnIndex);
}
if (partitionInfo.AvailableLeft)
{
prevL = Av1SymbolContextHelper.GetSegmentId(this.segmentIds, rowIndex, columnIndex - 1);
prevL = this.FrameInfo.GetSegmentId(rowIndex, columnIndex - 1);
}
if (prevU == -1)
@ -2191,7 +2302,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="reader">The tile symbol decoder.</param>
/// <param name="partitionInfo">The current coding block.</param>
/// <remarks>Implements AV1 section 5.11.56 and corresponds to <c>read_cdef</c> in libaom.</remarks>
private void ReadCdef(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo)
private void ReadCdef(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo)
{
if (partitionInfo.ModeInfo.Skip || this.FrameHeader.CodedLossless || !this.SequenceHeader.EnableCdef || this.FrameHeader.AllowIntraBlockCopy)
{
@ -2235,7 +2346,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// </summary>
/// <param name="reader">The tile symbol decoder.</param>
/// <param name="partitionInfo">The current coding block and superblock delta storage.</param>
private void ReadDeltaLoopFilter(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo)
private void ReadDeltaLoopFilter(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo)
{
if (!this.FrameHeader.DeltaLoopFilterParameters.IsPresent || partitionInfo.ModeInfo.PositionInSuperblock != Point.Empty)
{
@ -2273,7 +2384,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="reader">The tile symbol decoder.</param>
/// <param name="partitionInfo">The current coding block and its available neighbors.</param>
/// <returns><see langword="true"/> when the block omits residual coefficients; otherwise, <see langword="false"/>.</returns>
private bool ReadSkip(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo)
private bool ReadSkip(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo)
{
int segmentId = partitionInfo.ModeInfo.SegmentId;
if (this.FrameHeader.SegmentationParameters.SegmentIdPrecedesSkip &&
@ -2295,7 +2406,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="reader">The tile symbol decoder.</param>
/// <param name="partitionInfo">The current coding block and superblock quantizer storage.</param>
/// <remarks>Corresponds to <c>read_delta_qindex</c> in SVT-AV1.</remarks>
private void ReadDeltaQuantizerIndex(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo)
private void ReadDeltaQuantizerIndex(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo)
{
if (!this.FrameHeader.DeltaQParameters.IsPresent || partitionInfo.ModeInfo.PositionInSuperblock != Point.Empty)
{
@ -2416,4 +2527,17 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
}
}
}
/// <summary>
/// Provides inline storage for the two self-guided restoration coefficients of each of the three AV1 planes.
/// </summary>
/// <typeparam name="T">The stored value type.</typeparam>
[InlineArray(6)]
private struct InlineArray6<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
}

14
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs

@ -485,7 +485,7 @@ internal partial class Av1TileWriter
}
Av1PredictionMode intra_luma_mode = macroBlockModeInfo.Block.Mode;
Av1PredictionMode intra_chroma_mode = macroBlockModeInfo.Block.UvMode;
Av1ChromaPredictionMode intra_chroma_mode = macroBlockModeInfo.Block.UvMode;
if (IsIntraBlockCopyAllowed(pcs.Parent.FrameHeader/*, pcs.Parent.SliceType*/))
{
WriteIntraBlockCopyInfo(ref writer, macroBlockModeInfo, blk_ptr);
@ -624,12 +624,12 @@ internal partial class Av1TileWriter
Av1EncoderBlockStruct blk_ptr,
Av1BlockSize blockSize,
Av1PredictionMode lumaMode,
Av1PredictionMode chromaMode,
Av1ChromaPredictionMode chromaMode,
bool isChromaFromLumaAllowed)
{
writer.WriteChromaMode(chromaMode, isChromaFromLumaAllowed, lumaMode);
if (chromaMode == Av1PredictionMode.UvChromaFromLuma)
if (chromaMode == Av1ChromaPredictionMode.ChromaFromLuma)
{
writer.WriteChromaFromLumaAlphas(
blk_ptr.PredictionUnits[0].ChromaFromLumaIndex,
@ -638,7 +638,9 @@ internal partial class Av1TileWriter
if (blockSize >= Av1BlockSize.Block8x8 && macroBlockModeInfo.Block.UvMode.IsDirectional())
{
writer.WriteAngleDelta(blk_ptr.PredictionUnits[0].AngleDelta[(int)Av1PlaneType.Uv] + Av1Constants.MaxAngleDelta, chromaMode);
writer.WriteAngleDelta(
blk_ptr.PredictionUnits[0].AngleDelta[(int)Av1PlaneType.Uv] + Av1Constants.MaxAngleDelta,
chromaMode.ToLumaMode());
}
}
@ -712,7 +714,7 @@ internal partial class Av1TileWriter
Point point)
{/*
Av1PredictionMode intra_luma_mode = macroBlockModeInfo.Mode;
Av1PredictionMode intra_chroma_mode = macroBlockModeInfo.ModeUv;
Av1ChromaPredictionMode intra_chroma_mode = macroBlockModeInfo.Block.UvMode;
Av1PaletteModeInfo pmi = blk_ptr.PaletteInfo.pmi;
int bsize_ctx = svt_aom_get_palette_bsize_ctx(bsize);
@ -729,7 +731,7 @@ internal partial class Av1TileWriter
}
}
bool uv_dc_pred = intra_chroma_mode == Av1PredictionMode.DC && is_chroma_reference(point, blockSize, 1, 1);
bool uv_dc_pred = intra_chroma_mode == Av1ChromaPredictionMode.DC && is_chroma_reference(point, blockSize, 1, 1);
if (uv_dc_pred)
{
// assert(blk_ptr->palette_size[1] == 0); //remove when chroma is on

47
src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs

@ -106,15 +106,34 @@ internal sealed class Av1BlockDecoder : IDisposable
(this.sequenceHeader.ColorConfig.SubSamplingX ? ySize >> 2 : ySize) +
(this.sequenceHeader.ColorConfig.SubSamplingY ? ySize >> 2 : ySize);
this.inverseQuantizationOwner = this.frameBuffer.MemoryAllocator.Allocate<int>(inverseQuantizationSize);
this.transformWorkspaceOwner = this.frameBuffer.MemoryAllocator.Allocate<int>(Av1TransformWorkspace.MaximumLength);
this.predictionScratchOwner = this.frameBuffer.MemoryAllocator.Allocate<short>(Av1PredictionDecoder.ScratchLength);
this.predictionDecoder = new(sequenceHeader, frameHeader, this.predictionScratchOwner.Memory);
this.isLoopFilterEnabled = frameHeader.LoopFilterParameters.FilterLevel[0] != 0 ||
frameHeader.LoopFilterParameters.FilterLevel[1] != 0;
this.currentCoefficientIndex = new int[3];
this.chromaFromLumaContext = new(sequenceHeader.ColorConfig);
IMemoryOwner<int>? inverseQuantizationOwner = null;
IMemoryOwner<int>? transformWorkspaceOwner = null;
IMemoryOwner<short>? predictionScratchOwner = null;
try
{
inverseQuantizationOwner = this.frameBuffer.MemoryAllocator.Allocate<int>(inverseQuantizationSize);
transformWorkspaceOwner = this.frameBuffer.MemoryAllocator.Allocate<int>(Av1TransformWorkspace.MaximumLength);
predictionScratchOwner = this.frameBuffer.MemoryAllocator.Allocate<short>(Av1PredictionDecoder.ScratchLength);
this.inverseQuantizationOwner = inverseQuantizationOwner;
this.transformWorkspaceOwner = transformWorkspaceOwner;
this.predictionScratchOwner = predictionScratchOwner;
this.predictionDecoder = new(sequenceHeader, frameHeader, predictionScratchOwner.Memory);
this.isLoopFilterEnabled = frameHeader.LoopFilterParameters.FilterLevel[0] != 0 ||
frameHeader.LoopFilterParameters.FilterLevel[1] != 0;
this.currentCoefficientIndex = new int[3];
this.chromaFromLumaContext = new(sequenceHeader.ColorConfig);
}
catch
{
// A constructor that does not return transfers no ownership to its caller. Unwind successful rents in
// reverse order so allocator diagnostics and pooled buffers remain balanced after any later allocation.
predictionScratchOwner?.Dispose();
transformWorkspaceOwner?.Dispose();
inverseQuantizationOwner?.Dispose();
throw;
}
}
/// <summary>
@ -360,7 +379,7 @@ internal sealed class Av1BlockDecoder : IDisposable
if (highBitDepth)
{
this.predictionDecoder.Decode(
partitionInfo,
ref partitionInfo,
(Av1Plane)plane,
transformSize,
tileInfo,
@ -373,7 +392,7 @@ internal sealed class Av1BlockDecoder : IDisposable
else
{
this.predictionDecoder.Decode(
partitionInfo,
ref partitionInfo,
(Av1Plane)plane,
transformSize,
tileInfo,
@ -438,7 +457,7 @@ internal sealed class Av1BlockDecoder : IDisposable
}
// Store Luma for CFL if required!
if (plane == (int)Av1Plane.Y && StoreChromaFromLumaRequired(colorConfig, partitionInfo))
if (plane == (int)Av1Plane.Y && StoreChromaFromLumaRequired(colorConfig, ref partitionInfo))
{
// The predictor span begins on the previous row; CFL storage consumes reconstructed samples from
// the transform block itself, hence the explicit one-stride advance for both sample pipelines.
@ -562,7 +581,7 @@ internal sealed class Av1BlockDecoder : IDisposable
/// <returns>
/// <see langword="true"/> when chroma is present and the current luma block can contribute to a chroma-from-luma block.
/// </returns>
private static bool StoreChromaFromLumaRequired(ObuColorConfig colorConfig, Av1PartitionInfo partitionInfo)
private static bool StoreChromaFromLumaRequired(ObuColorConfig colorConfig, ref Av1PartitionInfo partitionInfo)
=> !colorConfig.IsMonochrome &&
(!partitionInfo.IsChroma || partitionInfo.ModeInfo.UvMode == Av1PredictionMode.UvChromaFromLuma);
(!partitionInfo.IsChroma || partitionInfo.ModeInfo.UvMode == Av1ChromaPredictionMode.ChromaFromLuma);
}

29
src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Adst16Operator.cs

@ -0,0 +1,29 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the sixteen-point asymmetric discrete sine forward transform operator.
/// </content>
internal static partial class Av1ForwardTransformer
{
/// <summary>
/// Defines the sixteen-point AV1 forward asymmetric discrete sine transform operator.
/// </summary>
internal readonly struct Adst16Operator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Adst16(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}
}

29
src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Adst4Operator.cs

@ -0,0 +1,29 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the four-point asymmetric discrete sine forward transform operator.
/// </content>
internal static partial class Av1ForwardTransformer
{
/// <summary>
/// Defines the four-point AV1 forward asymmetric discrete sine transform operator.
/// </summary>
internal readonly struct Adst4Operator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Adst4(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}
}

29
src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Adst8Operator.cs

@ -0,0 +1,29 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the eight-point asymmetric discrete sine forward transform operator.
/// </content>
internal static partial class Av1ForwardTransformer
{
/// <summary>
/// Defines the eight-point AV1 forward asymmetric discrete sine transform operator.
/// </summary>
internal readonly struct Adst8Operator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Adst8(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}
}

29
src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Dct16Operator.cs

@ -0,0 +1,29 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the sixteen-point discrete cosine forward transform operator.
/// </content>
internal static partial class Av1ForwardTransformer
{
/// <summary>
/// Defines the sixteen-point AV1 forward discrete cosine transform operator.
/// </summary>
internal readonly struct Dct16Operator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Dct16(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}
}

29
src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Dct32Operator.cs

@ -0,0 +1,29 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the thirty-two-point discrete cosine forward transform operator.
/// </content>
internal static partial class Av1ForwardTransformer
{
/// <summary>
/// Defines the thirty-two-point AV1 forward discrete cosine transform operator.
/// </summary>
internal readonly struct Dct32Operator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Dct32(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}
}

29
src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Dct4Operator.cs

@ -0,0 +1,29 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the four-point discrete cosine forward transform operator.
/// </content>
internal static partial class Av1ForwardTransformer
{
/// <summary>
/// Defines the four-point AV1 forward discrete cosine transform operator.
/// </summary>
internal readonly struct Dct4Operator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Dct4(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}
}

29
src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Dct64Operator.cs

@ -0,0 +1,29 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the sixty-four-point discrete cosine forward transform operator.
/// </content>
internal static partial class Av1ForwardTransformer
{
/// <summary>
/// Defines the sixty-four-point AV1 forward discrete cosine transform operator.
/// </summary>
internal readonly struct Dct64Operator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Dct64(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}
}

29
src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Dct8Operator.cs

@ -0,0 +1,29 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the eight-point discrete cosine forward transform operator.
/// </content>
internal static partial class Av1ForwardTransformer
{
/// <summary>
/// Defines the eight-point AV1 forward discrete cosine transform operator.
/// </summary>
internal readonly struct Dct8Operator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Dct8(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}
}

29
src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Identity16Operator.cs

@ -0,0 +1,29 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the sixteen-point identity forward transform operator.
/// </content>
internal static partial class Av1ForwardTransformer
{
/// <summary>
/// Defines the sixteen-point AV1 forward identity transform operator.
/// </summary>
internal readonly struct Identity16Operator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Identity16(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}
}

29
src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Identity32Operator.cs

@ -0,0 +1,29 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the thirty-two-point identity forward transform operator.
/// </content>
internal static partial class Av1ForwardTransformer
{
/// <summary>
/// Defines the thirty-two-point AV1 forward identity transform operator.
/// </summary>
internal readonly struct Identity32Operator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Identity32(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}
}

29
src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Identity4Operator.cs

@ -0,0 +1,29 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the four-point identity forward transform operator.
/// </content>
internal static partial class Av1ForwardTransformer
{
/// <summary>
/// Defines the four-point AV1 forward identity transform operator.
/// </summary>
internal readonly struct Identity4Operator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Identity4(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}
}

29
src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Identity8Operator.cs

@ -0,0 +1,29 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the eight-point identity forward transform operator.
/// </content>
internal static partial class Av1ForwardTransformer
{
/// <summary>
/// Defines the eight-point AV1 forward identity transform operator.
/// </summary>
internal readonly struct Identity8Operator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Identity8(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}
}

80
src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.cs → src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.Operator.cs

@ -18,8 +18,38 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// Eight-bit blocks use saturating 16-bit stages where their normative ranges permit it; high-bit-depth and scalar
/// fallback paths retain 32-bit stages. Both representations produce the same row-major coefficient contract.
/// </remarks>
internal static class Av1ForwardTransformer
internal static partial class Av1ForwardTransformer
{
/// <summary>
/// Defines one AV1 forward transform which can be specialized for the selected sample and SIMD lane type.
/// </summary>
/// <remarks>
/// A concrete operator identifies the transform stage network. The two-dimensional driver selects the sample type
/// and vector width once per block, allowing the JIT to specialize the complete network without interface dispatch
/// inside the transform stages.
/// </remarks>
internal interface IAv1ForwardTransform1dOperator
{
/// <summary>
/// Transforms the independent axes stored in each value lane.
/// </summary>
/// <typeparam name="TValue">The scalar or SIMD value containing the independent transform axes.</typeparam>
/// <param name="values">The first value in the strided transform block.</param>
/// <param name="inputStride">The byte distance between consecutive input positions.</param>
/// <param name="outputStride">The byte distance between consecutive output positions.</param>
/// <param name="buffer0">The first fixed transform-stage buffer.</param>
/// <param name="buffer1">The second fixed transform-stage buffer.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
public static abstract void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct;
}
/// <summary>
/// Resolves and applies the configured two-dimensional AV1 forward transform.
/// </summary>
@ -65,40 +95,40 @@ internal static class Av1ForwardTransformer
switch (config.TransformFunctionTypeColumn)
{
case Av1TransformFunctionType.Dct4:
DispatchRow<Av1Dct4Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
DispatchRow<Dct4Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Dct8:
DispatchRow<Av1Dct8Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
DispatchRow<Dct8Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Dct16:
DispatchRow<Av1Dct16Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
DispatchRow<Dct16Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Dct32:
DispatchRow<Av1Dct32Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
DispatchRow<Dct32Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Dct64:
DispatchRow<Av1Dct64Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
DispatchRow<Dct64Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Adst4:
DispatchRow<Av1Adst4Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
DispatchRow<Adst4Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Adst8:
DispatchRow<Av1Adst8Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
DispatchRow<Adst8Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Adst16:
DispatchRow<Av1Adst16Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
DispatchRow<Adst16Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Identity4:
DispatchRow<Av1Identity4Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
DispatchRow<Identity4Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Identity8:
DispatchRow<Av1Identity8Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
DispatchRow<Identity8Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Identity16:
DispatchRow<Av1Identity16Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
DispatchRow<Identity16Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Identity32:
DispatchRow<Av1Identity32Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
DispatchRow<Identity32Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
default:
throw new InvalidImageContentException($"The {config.TransformFunctionTypeColumn} column transform is not valid for {config.TransformSize}.");
@ -127,40 +157,40 @@ internal static class Av1ForwardTransformer
switch (config.TransformFunctionTypeRow)
{
case Av1TransformFunctionType.Dct4:
Transform2d<TColumnOperator, Av1Dct4Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
Transform2d<TColumnOperator, Dct4Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Dct8:
Transform2d<TColumnOperator, Av1Dct8Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
Transform2d<TColumnOperator, Dct8Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Dct16:
Transform2d<TColumnOperator, Av1Dct16Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
Transform2d<TColumnOperator, Dct16Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Dct32:
Transform2d<TColumnOperator, Av1Dct32Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
Transform2d<TColumnOperator, Dct32Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Dct64:
Transform2d<TColumnOperator, Av1Dct64Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
Transform2d<TColumnOperator, Dct64Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Adst4:
Transform2d<TColumnOperator, Av1Adst4Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
Transform2d<TColumnOperator, Adst4Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Adst8:
Transform2d<TColumnOperator, Av1Adst8Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
Transform2d<TColumnOperator, Adst8Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Adst16:
Transform2d<TColumnOperator, Av1Adst16Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
Transform2d<TColumnOperator, Adst16Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Identity4:
Transform2d<TColumnOperator, Av1Identity4Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
Transform2d<TColumnOperator, Identity4Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Identity8:
Transform2d<TColumnOperator, Av1Identity8Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
Transform2d<TColumnOperator, Identity8Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Identity16:
Transform2d<TColumnOperator, Av1Identity16Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
Transform2d<TColumnOperator, Identity16Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
case Av1TransformFunctionType.Identity32:
Transform2d<TColumnOperator, Av1Identity32Forward1dOperator>(input, coefficients, stride, bitDepth, ref config, workspace);
Transform2d<TColumnOperator, Identity32Operator>(input, coefficients, stride, bitDepth, ref config, workspace);
break;
default:
throw new InvalidImageContentException($"The {config.TransformFunctionTypeRow} row transform is not valid for {config.TransformSize}.");

574
src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Adst16Operator.cs

@ -0,0 +1,574 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the sixteen-point asymmetric discrete sine inverse transform operator.
/// </content>
internal static partial class Av1Inverse2dTransformer
{
/// <summary>
/// Defines the 16-point AV1 inverse asymmetric discrete sine transform operator.
/// </summary>
/// <remarks>
/// Vector fields represent transform positions and vector lanes represent independent axes. The SIMD overloads apply
/// the same staged rotations, fixed-point rounding, and range clamps as the scalar overload without mixing axes.
/// </remarks>
internal readonly struct Adst16Operator : IAv1InverseTransform1dOperator
{
/// <summary>
/// Applies the normative 16-point AV1 inverse asymmetric discrete sine transform.
/// </summary>
/// <param name="input">The sixteen frequency-domain coefficients.</param>
/// <param name="output">The sixteen spatial-domain residual values.</param>
/// <param name="step">The sixteen-element stage buffer owned by the containing two-dimensional transform.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0;
// Stage 1 permutes the coefficients into the signed order used by the ADST factorization.
stage++;
output[0] = input[15];
output[1] = input[0];
output[2] = input[13];
output[3] = input[2];
output[4] = input[11];
output[5] = input[4];
output[6] = input[9];
output[7] = input[6];
output[8] = input[7];
output[9] = input[8];
output[10] = input[5];
output[11] = input[10];
output[12] = input[3];
output[13] = input[12];
output[14] = input[1];
output[15] = input[14];
// Stage 2 applies the terminal odd-angle rotations in reverse.
stage++;
step[0] = Av1Transform1dMath.HalfButterfly(cospi[2], output[0], cospi[62], output[1], cosBit);
step[1] = Av1Transform1dMath.HalfButterfly(cospi[62], output[0], -cospi[2], output[1], cosBit);
step[2] = Av1Transform1dMath.HalfButterfly(cospi[10], output[2], cospi[54], output[3], cosBit);
step[3] = Av1Transform1dMath.HalfButterfly(cospi[54], output[2], -cospi[10], output[3], cosBit);
step[4] = Av1Transform1dMath.HalfButterfly(cospi[18], output[4], cospi[46], output[5], cosBit);
step[5] = Av1Transform1dMath.HalfButterfly(cospi[46], output[4], -cospi[18], output[5], cosBit);
step[6] = Av1Transform1dMath.HalfButterfly(cospi[26], output[6], cospi[38], output[7], cosBit);
step[7] = Av1Transform1dMath.HalfButterfly(cospi[38], output[6], -cospi[26], output[7], cosBit);
step[8] = Av1Transform1dMath.HalfButterfly(cospi[34], output[8], cospi[30], output[9], cosBit);
step[9] = Av1Transform1dMath.HalfButterfly(cospi[30], output[8], -cospi[34], output[9], cosBit);
step[10] = Av1Transform1dMath.HalfButterfly(cospi[42], output[10], cospi[22], output[11], cosBit);
step[11] = Av1Transform1dMath.HalfButterfly(cospi[22], output[10], -cospi[42], output[11], cosBit);
step[12] = Av1Transform1dMath.HalfButterfly(cospi[50], output[12], cospi[14], output[13], cosBit);
step[13] = Av1Transform1dMath.HalfButterfly(cospi[14], output[12], -cospi[50], output[13], cosBit);
step[14] = Av1Transform1dMath.HalfButterfly(cospi[58], output[14], cospi[6], output[15], cosBit);
step[15] = Av1Transform1dMath.HalfButterfly(cospi[6], output[14], -cospi[58], output[15], cosBit);
// Stage 3 separates the complete butterfly into two eight-sample halves and clamps each lane.
stage++;
output[0] = Av1Transform1dMath.Clamp(step[0] + step[8], stageRange[stage]);
output[1] = Av1Transform1dMath.Clamp(step[1] + step[9], stageRange[stage]);
output[2] = Av1Transform1dMath.Clamp(step[2] + step[10], stageRange[stage]);
output[3] = Av1Transform1dMath.Clamp(step[3] + step[11], stageRange[stage]);
output[4] = Av1Transform1dMath.Clamp(step[4] + step[12], stageRange[stage]);
output[5] = Av1Transform1dMath.Clamp(step[5] + step[13], stageRange[stage]);
output[6] = Av1Transform1dMath.Clamp(step[6] + step[14], stageRange[stage]);
output[7] = Av1Transform1dMath.Clamp(step[7] + step[15], stageRange[stage]);
output[8] = Av1Transform1dMath.Clamp(step[0] - step[8], stageRange[stage]);
output[9] = Av1Transform1dMath.Clamp(step[1] - step[9], stageRange[stage]);
output[10] = Av1Transform1dMath.Clamp(step[2] - step[10], stageRange[stage]);
output[11] = Av1Transform1dMath.Clamp(step[3] - step[11], stageRange[stage]);
output[12] = Av1Transform1dMath.Clamp(step[4] - step[12], stageRange[stage]);
output[13] = Av1Transform1dMath.Clamp(step[5] - step[13], stageRange[stage]);
output[14] = Av1Transform1dMath.Clamp(step[6] - step[14], stageRange[stage]);
output[15] = Av1Transform1dMath.Clamp(step[7] - step[15], stageRange[stage]);
// Stage 4 reverses the pi/16 rotations in the upper half.
stage++;
step[0] = output[0];
step[1] = output[1];
step[2] = output[2];
step[3] = output[3];
step[4] = output[4];
step[5] = output[5];
step[6] = output[6];
step[7] = output[7];
step[8] = Av1Transform1dMath.HalfButterfly(cospi[8], output[8], cospi[56], output[9], cosBit);
step[9] = Av1Transform1dMath.HalfButterfly(cospi[56], output[8], -cospi[8], output[9], cosBit);
step[10] = Av1Transform1dMath.HalfButterfly(cospi[40], output[10], cospi[24], output[11], cosBit);
step[11] = Av1Transform1dMath.HalfButterfly(cospi[24], output[10], -cospi[40], output[11], cosBit);
step[12] = Av1Transform1dMath.HalfButterfly(-cospi[56], output[12], cospi[8], output[13], cosBit);
step[13] = Av1Transform1dMath.HalfButterfly(cospi[8], output[12], cospi[56], output[13], cosBit);
step[14] = Av1Transform1dMath.HalfButterfly(-cospi[24], output[14], cospi[40], output[15], cosBit);
step[15] = Av1Transform1dMath.HalfButterfly(cospi[40], output[14], cospi[24], output[15], cosBit);
// Stage 5 separates each eight-sample half into four-sample groups and clamps each lane.
stage++;
output[0] = Av1Transform1dMath.Clamp(step[0] + step[4], stageRange[stage]);
output[1] = Av1Transform1dMath.Clamp(step[1] + step[5], stageRange[stage]);
output[2] = Av1Transform1dMath.Clamp(step[2] + step[6], stageRange[stage]);
output[3] = Av1Transform1dMath.Clamp(step[3] + step[7], stageRange[stage]);
output[4] = Av1Transform1dMath.Clamp(step[0] - step[4], stageRange[stage]);
output[5] = Av1Transform1dMath.Clamp(step[1] - step[5], stageRange[stage]);
output[6] = Av1Transform1dMath.Clamp(step[2] - step[6], stageRange[stage]);
output[7] = Av1Transform1dMath.Clamp(step[3] - step[7], stageRange[stage]);
output[8] = Av1Transform1dMath.Clamp(step[8] + step[12], stageRange[stage]);
output[9] = Av1Transform1dMath.Clamp(step[9] + step[13], stageRange[stage]);
output[10] = Av1Transform1dMath.Clamp(step[10] + step[14], stageRange[stage]);
output[11] = Av1Transform1dMath.Clamp(step[11] + step[15], stageRange[stage]);
output[12] = Av1Transform1dMath.Clamp(step[8] - step[12], stageRange[stage]);
output[13] = Av1Transform1dMath.Clamp(step[9] - step[13], stageRange[stage]);
output[14] = Av1Transform1dMath.Clamp(step[10] - step[14], stageRange[stage]);
output[15] = Av1Transform1dMath.Clamp(step[11] - step[15], stageRange[stage]);
// Stage 6 reverses the pi/8 and 3pi/8 rotations.
stage++;
step[0] = output[0];
step[1] = output[1];
step[2] = output[2];
step[3] = output[3];
step[4] = Av1Transform1dMath.HalfButterfly(cospi[16], output[4], cospi[48], output[5], cosBit);
step[5] = Av1Transform1dMath.HalfButterfly(cospi[48], output[4], -cospi[16], output[5], cosBit);
step[6] = Av1Transform1dMath.HalfButterfly(-cospi[48], output[6], cospi[16], output[7], cosBit);
step[7] = Av1Transform1dMath.HalfButterfly(cospi[16], output[6], cospi[48], output[7], cosBit);
step[8] = output[8];
step[9] = output[9];
step[10] = output[10];
step[11] = output[11];
step[12] = Av1Transform1dMath.HalfButterfly(cospi[16], output[12], cospi[48], output[13], cosBit);
step[13] = Av1Transform1dMath.HalfButterfly(cospi[48], output[12], -cospi[16], output[13], cosBit);
step[14] = Av1Transform1dMath.HalfButterfly(-cospi[48], output[14], cospi[16], output[15], cosBit);
step[15] = Av1Transform1dMath.HalfButterfly(cospi[16], output[14], cospi[48], output[15], cosBit);
// Stage 7 separates the four-sample groups into adjacent coefficient pairs and clamps each lane.
stage++;
output[0] = Av1Transform1dMath.Clamp(step[0] + step[2], stageRange[stage]);
output[1] = Av1Transform1dMath.Clamp(step[1] + step[3], stageRange[stage]);
output[2] = Av1Transform1dMath.Clamp(step[0] - step[2], stageRange[stage]);
output[3] = Av1Transform1dMath.Clamp(step[1] - step[3], stageRange[stage]);
output[4] = Av1Transform1dMath.Clamp(step[4] + step[6], stageRange[stage]);
output[5] = Av1Transform1dMath.Clamp(step[5] + step[7], stageRange[stage]);
output[6] = Av1Transform1dMath.Clamp(step[4] - step[6], stageRange[stage]);
output[7] = Av1Transform1dMath.Clamp(step[5] - step[7], stageRange[stage]);
output[8] = Av1Transform1dMath.Clamp(step[8] + step[10], stageRange[stage]);
output[9] = Av1Transform1dMath.Clamp(step[9] + step[11], stageRange[stage]);
output[10] = Av1Transform1dMath.Clamp(step[8] - step[10], stageRange[stage]);
output[11] = Av1Transform1dMath.Clamp(step[9] - step[11], stageRange[stage]);
output[12] = Av1Transform1dMath.Clamp(step[12] + step[14], stageRange[stage]);
output[13] = Av1Transform1dMath.Clamp(step[13] + step[15], stageRange[stage]);
output[14] = Av1Transform1dMath.Clamp(step[12] - step[14], stageRange[stage]);
output[15] = Av1Transform1dMath.Clamp(step[13] - step[15], stageRange[stage]);
// Stage 8 reverses the pi/4 rotations for the middle pairs.
step[0] = output[0];
step[1] = output[1];
step[2] = Av1Transform1dMath.HalfButterfly(cospi[32], output[2], cospi[32], output[3], cosBit);
step[3] = Av1Transform1dMath.HalfButterfly(cospi[32], output[2], -cospi[32], output[3], cosBit);
step[4] = output[4];
step[5] = output[5];
step[6] = Av1Transform1dMath.HalfButterfly(cospi[32], output[6], cospi[32], output[7], cosBit);
step[7] = Av1Transform1dMath.HalfButterfly(cospi[32], output[6], -cospi[32], output[7], cosBit);
step[8] = output[8];
step[9] = output[9];
step[10] = Av1Transform1dMath.HalfButterfly(cospi[32], output[10], cospi[32], output[11], cosBit);
step[11] = Av1Transform1dMath.HalfButterfly(cospi[32], output[10], -cospi[32], output[11], cosBit);
step[12] = output[12];
step[13] = output[13];
step[14] = Av1Transform1dMath.HalfButterfly(cospi[32], output[14], cospi[32], output[15], cosBit);
step[15] = Av1Transform1dMath.HalfButterfly(cospi[32], output[14], -cospi[32], output[15], cosBit);
// Stage 9 applies the AV1 signs and permutation that restore spatial sample order.
output[0] = step[0];
output[1] = -step[8];
output[2] = step[12];
output[3] = -step[4];
output[4] = step[6];
output[5] = -step[14];
output[6] = step[10];
output[7] = -step[2];
output[8] = step[3];
output[9] = -step[11];
output[10] = step[15];
output[11] = -step[7];
output[12] = step[5];
output[13] = -step[13];
output[14] = step[9];
output[15] = -step[1];
}
/// <inheritdoc/>
public static void Transform(
ref Av1TransformVector<Vector256<int>> input,
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0;
// Stage 1 permutes the coefficients into the signed order used by the ADST factorization.
stage++;
output.V0 = input.V15;
output.V1 = input.V0;
output.V2 = input.V13;
output.V3 = input.V2;
output.V4 = input.V11;
output.V5 = input.V4;
output.V6 = input.V9;
output.V7 = input.V6;
output.V8 = input.V7;
output.V9 = input.V8;
output.V10 = input.V5;
output.V11 = input.V10;
output.V12 = input.V3;
output.V13 = input.V12;
output.V14 = input.V1;
output.V15 = input.V14;
// Stage 2 applies the terminal odd-angle rotations in reverse.
stage++;
step.V0 = Av1Transform1dMath.HalfButterfly(cospi[2], output.V0, cospi[62], output.V1, cosBit);
step.V1 = Av1Transform1dMath.HalfButterfly(cospi[62], output.V0, -cospi[2], output.V1, cosBit);
step.V2 = Av1Transform1dMath.HalfButterfly(cospi[10], output.V2, cospi[54], output.V3, cosBit);
step.V3 = Av1Transform1dMath.HalfButterfly(cospi[54], output.V2, -cospi[10], output.V3, cosBit);
step.V4 = Av1Transform1dMath.HalfButterfly(cospi[18], output.V4, cospi[46], output.V5, cosBit);
step.V5 = Av1Transform1dMath.HalfButterfly(cospi[46], output.V4, -cospi[18], output.V5, cosBit);
step.V6 = Av1Transform1dMath.HalfButterfly(cospi[26], output.V6, cospi[38], output.V7, cosBit);
step.V7 = Av1Transform1dMath.HalfButterfly(cospi[38], output.V6, -cospi[26], output.V7, cosBit);
step.V8 = Av1Transform1dMath.HalfButterfly(cospi[34], output.V8, cospi[30], output.V9, cosBit);
step.V9 = Av1Transform1dMath.HalfButterfly(cospi[30], output.V8, -cospi[34], output.V9, cosBit);
step.V10 = Av1Transform1dMath.HalfButterfly(cospi[42], output.V10, cospi[22], output.V11, cosBit);
step.V11 = Av1Transform1dMath.HalfButterfly(cospi[22], output.V10, -cospi[42], output.V11, cosBit);
step.V12 = Av1Transform1dMath.HalfButterfly(cospi[50], output.V12, cospi[14], output.V13, cosBit);
step.V13 = Av1Transform1dMath.HalfButterfly(cospi[14], output.V12, -cospi[50], output.V13, cosBit);
step.V14 = Av1Transform1dMath.HalfButterfly(cospi[58], output.V14, cospi[6], output.V15, cosBit);
step.V15 = Av1Transform1dMath.HalfButterfly(cospi[6], output.V14, -cospi[58], output.V15, cosBit);
// Stage 3 separates the complete butterfly into two eight-sample halves and clamps each lane.
stage++;
output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V8, stageRange[stage]);
output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V9, stageRange[stage]);
output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V10, stageRange[stage]);
output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V11, stageRange[stage]);
output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V12, stageRange[stage]);
output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V13, stageRange[stage]);
output.V6 = Av1Transform1dMath.Clamp(step.V6 + step.V14, stageRange[stage]);
output.V7 = Av1Transform1dMath.Clamp(step.V7 + step.V15, stageRange[stage]);
output.V8 = Av1Transform1dMath.Clamp(step.V0 - step.V8, stageRange[stage]);
output.V9 = Av1Transform1dMath.Clamp(step.V1 - step.V9, stageRange[stage]);
output.V10 = Av1Transform1dMath.Clamp(step.V2 - step.V10, stageRange[stage]);
output.V11 = Av1Transform1dMath.Clamp(step.V3 - step.V11, stageRange[stage]);
output.V12 = Av1Transform1dMath.Clamp(step.V4 - step.V12, stageRange[stage]);
output.V13 = Av1Transform1dMath.Clamp(step.V5 - step.V13, stageRange[stage]);
output.V14 = Av1Transform1dMath.Clamp(step.V6 - step.V14, stageRange[stage]);
output.V15 = Av1Transform1dMath.Clamp(step.V7 - step.V15, stageRange[stage]);
// Stage 4 reverses the pi/16 rotations in the upper half.
stage++;
step.V0 = output.V0;
step.V1 = output.V1;
step.V2 = output.V2;
step.V3 = output.V3;
step.V4 = output.V4;
step.V5 = output.V5;
step.V6 = output.V6;
step.V7 = output.V7;
step.V8 = Av1Transform1dMath.HalfButterfly(cospi[8], output.V8, cospi[56], output.V9, cosBit);
step.V9 = Av1Transform1dMath.HalfButterfly(cospi[56], output.V8, -cospi[8], output.V9, cosBit);
step.V10 = Av1Transform1dMath.HalfButterfly(cospi[40], output.V10, cospi[24], output.V11, cosBit);
step.V11 = Av1Transform1dMath.HalfButterfly(cospi[24], output.V10, -cospi[40], output.V11, cosBit);
step.V12 = Av1Transform1dMath.HalfButterfly(-cospi[56], output.V12, cospi[8], output.V13, cosBit);
step.V13 = Av1Transform1dMath.HalfButterfly(cospi[8], output.V12, cospi[56], output.V13, cosBit);
step.V14 = Av1Transform1dMath.HalfButterfly(-cospi[24], output.V14, cospi[40], output.V15, cosBit);
step.V15 = Av1Transform1dMath.HalfButterfly(cospi[40], output.V14, cospi[24], output.V15, cosBit);
// Stage 5 separates each eight-sample half into four-sample groups and clamps each lane.
stage++;
output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V4, stageRange[stage]);
output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V5, stageRange[stage]);
output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V6, stageRange[stage]);
output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V7, stageRange[stage]);
output.V4 = Av1Transform1dMath.Clamp(step.V0 - step.V4, stageRange[stage]);
output.V5 = Av1Transform1dMath.Clamp(step.V1 - step.V5, stageRange[stage]);
output.V6 = Av1Transform1dMath.Clamp(step.V2 - step.V6, stageRange[stage]);
output.V7 = Av1Transform1dMath.Clamp(step.V3 - step.V7, stageRange[stage]);
output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V12, stageRange[stage]);
output.V9 = Av1Transform1dMath.Clamp(step.V9 + step.V13, stageRange[stage]);
output.V10 = Av1Transform1dMath.Clamp(step.V10 + step.V14, stageRange[stage]);
output.V11 = Av1Transform1dMath.Clamp(step.V11 + step.V15, stageRange[stage]);
output.V12 = Av1Transform1dMath.Clamp(step.V8 - step.V12, stageRange[stage]);
output.V13 = Av1Transform1dMath.Clamp(step.V9 - step.V13, stageRange[stage]);
output.V14 = Av1Transform1dMath.Clamp(step.V10 - step.V14, stageRange[stage]);
output.V15 = Av1Transform1dMath.Clamp(step.V11 - step.V15, stageRange[stage]);
// Stage 6 reverses the pi/8 and 3pi/8 rotations.
stage++;
step.V0 = output.V0;
step.V1 = output.V1;
step.V2 = output.V2;
step.V3 = output.V3;
step.V4 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V4, cospi[48], output.V5, cosBit);
step.V5 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V4, -cospi[16], output.V5, cosBit);
step.V6 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V6, cospi[16], output.V7, cosBit);
step.V7 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V6, cospi[48], output.V7, cosBit);
step.V8 = output.V8;
step.V9 = output.V9;
step.V10 = output.V10;
step.V11 = output.V11;
step.V12 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V12, cospi[48], output.V13, cosBit);
step.V13 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V12, -cospi[16], output.V13, cosBit);
step.V14 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V14, cospi[16], output.V15, cosBit);
step.V15 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V14, cospi[48], output.V15, cosBit);
// Stage 7 separates the four-sample groups into adjacent coefficient pairs and clamps each lane.
stage++;
output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V2, stageRange[stage]);
output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V3, stageRange[stage]);
output.V2 = Av1Transform1dMath.Clamp(step.V0 - step.V2, stageRange[stage]);
output.V3 = Av1Transform1dMath.Clamp(step.V1 - step.V3, stageRange[stage]);
output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V6, stageRange[stage]);
output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V7, stageRange[stage]);
output.V6 = Av1Transform1dMath.Clamp(step.V4 - step.V6, stageRange[stage]);
output.V7 = Av1Transform1dMath.Clamp(step.V5 - step.V7, stageRange[stage]);
output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V10, stageRange[stage]);
output.V9 = Av1Transform1dMath.Clamp(step.V9 + step.V11, stageRange[stage]);
output.V10 = Av1Transform1dMath.Clamp(step.V8 - step.V10, stageRange[stage]);
output.V11 = Av1Transform1dMath.Clamp(step.V9 - step.V11, stageRange[stage]);
output.V12 = Av1Transform1dMath.Clamp(step.V12 + step.V14, stageRange[stage]);
output.V13 = Av1Transform1dMath.Clamp(step.V13 + step.V15, stageRange[stage]);
output.V14 = Av1Transform1dMath.Clamp(step.V12 - step.V14, stageRange[stage]);
output.V15 = Av1Transform1dMath.Clamp(step.V13 - step.V15, stageRange[stage]);
// Stage 8 reverses the pi/4 rotations for the middle pairs.
step.V0 = output.V0;
step.V1 = output.V1;
step.V2 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V2, cospi[32], output.V3, cosBit);
step.V3 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V2, -cospi[32], output.V3, cosBit);
step.V4 = output.V4;
step.V5 = output.V5;
step.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V6, cospi[32], output.V7, cosBit);
step.V7 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V6, -cospi[32], output.V7, cosBit);
step.V8 = output.V8;
step.V9 = output.V9;
step.V10 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V10, cospi[32], output.V11, cosBit);
step.V11 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V10, -cospi[32], output.V11, cosBit);
step.V12 = output.V12;
step.V13 = output.V13;
step.V14 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V14, cospi[32], output.V15, cosBit);
step.V15 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V14, -cospi[32], output.V15, cosBit);
// Stage 9 applies the AV1 signs and permutation that restore spatial sample order.
output.V0 = step.V0;
output.V1 = -step.V8;
output.V2 = step.V12;
output.V3 = -step.V4;
output.V4 = step.V6;
output.V5 = -step.V14;
output.V6 = step.V10;
output.V7 = -step.V2;
output.V8 = step.V3;
output.V9 = -step.V11;
output.V10 = step.V15;
output.V11 = -step.V7;
output.V12 = step.V5;
output.V13 = -step.V13;
output.V14 = step.V9;
output.V15 = -step.V1;
}
/// <summary>
/// Applies the transform to four independent axes in parallel.
/// </summary>
/// <param name="input">The source values for the parallel transform axes.</param>
/// <param name="output">The destination values for the parallel transform axes.</param>
/// <param name="step">The fixed stage storage for the parallel transform axes.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(
ref Av1TransformVector<Vector128<int>> input,
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0;
// Stage 1 permutes the coefficients into the signed order used by the ADST factorization.
stage++;
output.V0 = input.V15;
output.V1 = input.V0;
output.V2 = input.V13;
output.V3 = input.V2;
output.V4 = input.V11;
output.V5 = input.V4;
output.V6 = input.V9;
output.V7 = input.V6;
output.V8 = input.V7;
output.V9 = input.V8;
output.V10 = input.V5;
output.V11 = input.V10;
output.V12 = input.V3;
output.V13 = input.V12;
output.V14 = input.V1;
output.V15 = input.V14;
// Stage 2 applies the terminal odd-angle rotations in reverse.
stage++;
step.V0 = Av1Transform1dMath.HalfButterfly(cospi[2], output.V0, cospi[62], output.V1, cosBit);
step.V1 = Av1Transform1dMath.HalfButterfly(cospi[62], output.V0, -cospi[2], output.V1, cosBit);
step.V2 = Av1Transform1dMath.HalfButterfly(cospi[10], output.V2, cospi[54], output.V3, cosBit);
step.V3 = Av1Transform1dMath.HalfButterfly(cospi[54], output.V2, -cospi[10], output.V3, cosBit);
step.V4 = Av1Transform1dMath.HalfButterfly(cospi[18], output.V4, cospi[46], output.V5, cosBit);
step.V5 = Av1Transform1dMath.HalfButterfly(cospi[46], output.V4, -cospi[18], output.V5, cosBit);
step.V6 = Av1Transform1dMath.HalfButterfly(cospi[26], output.V6, cospi[38], output.V7, cosBit);
step.V7 = Av1Transform1dMath.HalfButterfly(cospi[38], output.V6, -cospi[26], output.V7, cosBit);
step.V8 = Av1Transform1dMath.HalfButterfly(cospi[34], output.V8, cospi[30], output.V9, cosBit);
step.V9 = Av1Transform1dMath.HalfButterfly(cospi[30], output.V8, -cospi[34], output.V9, cosBit);
step.V10 = Av1Transform1dMath.HalfButterfly(cospi[42], output.V10, cospi[22], output.V11, cosBit);
step.V11 = Av1Transform1dMath.HalfButterfly(cospi[22], output.V10, -cospi[42], output.V11, cosBit);
step.V12 = Av1Transform1dMath.HalfButterfly(cospi[50], output.V12, cospi[14], output.V13, cosBit);
step.V13 = Av1Transform1dMath.HalfButterfly(cospi[14], output.V12, -cospi[50], output.V13, cosBit);
step.V14 = Av1Transform1dMath.HalfButterfly(cospi[58], output.V14, cospi[6], output.V15, cosBit);
step.V15 = Av1Transform1dMath.HalfButterfly(cospi[6], output.V14, -cospi[58], output.V15, cosBit);
// Stage 3 separates the complete butterfly into two eight-sample halves and clamps each lane.
stage++;
output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V8, stageRange[stage]);
output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V9, stageRange[stage]);
output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V10, stageRange[stage]);
output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V11, stageRange[stage]);
output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V12, stageRange[stage]);
output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V13, stageRange[stage]);
output.V6 = Av1Transform1dMath.Clamp(step.V6 + step.V14, stageRange[stage]);
output.V7 = Av1Transform1dMath.Clamp(step.V7 + step.V15, stageRange[stage]);
output.V8 = Av1Transform1dMath.Clamp(step.V0 - step.V8, stageRange[stage]);
output.V9 = Av1Transform1dMath.Clamp(step.V1 - step.V9, stageRange[stage]);
output.V10 = Av1Transform1dMath.Clamp(step.V2 - step.V10, stageRange[stage]);
output.V11 = Av1Transform1dMath.Clamp(step.V3 - step.V11, stageRange[stage]);
output.V12 = Av1Transform1dMath.Clamp(step.V4 - step.V12, stageRange[stage]);
output.V13 = Av1Transform1dMath.Clamp(step.V5 - step.V13, stageRange[stage]);
output.V14 = Av1Transform1dMath.Clamp(step.V6 - step.V14, stageRange[stage]);
output.V15 = Av1Transform1dMath.Clamp(step.V7 - step.V15, stageRange[stage]);
// Stage 4 reverses the pi/16 rotations in the upper half.
stage++;
step.V0 = output.V0;
step.V1 = output.V1;
step.V2 = output.V2;
step.V3 = output.V3;
step.V4 = output.V4;
step.V5 = output.V5;
step.V6 = output.V6;
step.V7 = output.V7;
step.V8 = Av1Transform1dMath.HalfButterfly(cospi[8], output.V8, cospi[56], output.V9, cosBit);
step.V9 = Av1Transform1dMath.HalfButterfly(cospi[56], output.V8, -cospi[8], output.V9, cosBit);
step.V10 = Av1Transform1dMath.HalfButterfly(cospi[40], output.V10, cospi[24], output.V11, cosBit);
step.V11 = Av1Transform1dMath.HalfButterfly(cospi[24], output.V10, -cospi[40], output.V11, cosBit);
step.V12 = Av1Transform1dMath.HalfButterfly(-cospi[56], output.V12, cospi[8], output.V13, cosBit);
step.V13 = Av1Transform1dMath.HalfButterfly(cospi[8], output.V12, cospi[56], output.V13, cosBit);
step.V14 = Av1Transform1dMath.HalfButterfly(-cospi[24], output.V14, cospi[40], output.V15, cosBit);
step.V15 = Av1Transform1dMath.HalfButterfly(cospi[40], output.V14, cospi[24], output.V15, cosBit);
// Stage 5 separates each eight-sample half into four-sample groups and clamps each lane.
stage++;
output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V4, stageRange[stage]);
output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V5, stageRange[stage]);
output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V6, stageRange[stage]);
output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V7, stageRange[stage]);
output.V4 = Av1Transform1dMath.Clamp(step.V0 - step.V4, stageRange[stage]);
output.V5 = Av1Transform1dMath.Clamp(step.V1 - step.V5, stageRange[stage]);
output.V6 = Av1Transform1dMath.Clamp(step.V2 - step.V6, stageRange[stage]);
output.V7 = Av1Transform1dMath.Clamp(step.V3 - step.V7, stageRange[stage]);
output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V12, stageRange[stage]);
output.V9 = Av1Transform1dMath.Clamp(step.V9 + step.V13, stageRange[stage]);
output.V10 = Av1Transform1dMath.Clamp(step.V10 + step.V14, stageRange[stage]);
output.V11 = Av1Transform1dMath.Clamp(step.V11 + step.V15, stageRange[stage]);
output.V12 = Av1Transform1dMath.Clamp(step.V8 - step.V12, stageRange[stage]);
output.V13 = Av1Transform1dMath.Clamp(step.V9 - step.V13, stageRange[stage]);
output.V14 = Av1Transform1dMath.Clamp(step.V10 - step.V14, stageRange[stage]);
output.V15 = Av1Transform1dMath.Clamp(step.V11 - step.V15, stageRange[stage]);
// Stage 6 reverses the pi/8 and 3pi/8 rotations.
stage++;
step.V0 = output.V0;
step.V1 = output.V1;
step.V2 = output.V2;
step.V3 = output.V3;
step.V4 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V4, cospi[48], output.V5, cosBit);
step.V5 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V4, -cospi[16], output.V5, cosBit);
step.V6 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V6, cospi[16], output.V7, cosBit);
step.V7 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V6, cospi[48], output.V7, cosBit);
step.V8 = output.V8;
step.V9 = output.V9;
step.V10 = output.V10;
step.V11 = output.V11;
step.V12 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V12, cospi[48], output.V13, cosBit);
step.V13 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V12, -cospi[16], output.V13, cosBit);
step.V14 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V14, cospi[16], output.V15, cosBit);
step.V15 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V14, cospi[48], output.V15, cosBit);
// Stage 7 separates the four-sample groups into adjacent coefficient pairs and clamps each lane.
stage++;
output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V2, stageRange[stage]);
output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V3, stageRange[stage]);
output.V2 = Av1Transform1dMath.Clamp(step.V0 - step.V2, stageRange[stage]);
output.V3 = Av1Transform1dMath.Clamp(step.V1 - step.V3, stageRange[stage]);
output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V6, stageRange[stage]);
output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V7, stageRange[stage]);
output.V6 = Av1Transform1dMath.Clamp(step.V4 - step.V6, stageRange[stage]);
output.V7 = Av1Transform1dMath.Clamp(step.V5 - step.V7, stageRange[stage]);
output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V10, stageRange[stage]);
output.V9 = Av1Transform1dMath.Clamp(step.V9 + step.V11, stageRange[stage]);
output.V10 = Av1Transform1dMath.Clamp(step.V8 - step.V10, stageRange[stage]);
output.V11 = Av1Transform1dMath.Clamp(step.V9 - step.V11, stageRange[stage]);
output.V12 = Av1Transform1dMath.Clamp(step.V12 + step.V14, stageRange[stage]);
output.V13 = Av1Transform1dMath.Clamp(step.V13 + step.V15, stageRange[stage]);
output.V14 = Av1Transform1dMath.Clamp(step.V12 - step.V14, stageRange[stage]);
output.V15 = Av1Transform1dMath.Clamp(step.V13 - step.V15, stageRange[stage]);
// Stage 8 reverses the pi/4 rotations for the middle pairs.
step.V0 = output.V0;
step.V1 = output.V1;
step.V2 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V2, cospi[32], output.V3, cosBit);
step.V3 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V2, -cospi[32], output.V3, cosBit);
step.V4 = output.V4;
step.V5 = output.V5;
step.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V6, cospi[32], output.V7, cosBit);
step.V7 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V6, -cospi[32], output.V7, cosBit);
step.V8 = output.V8;
step.V9 = output.V9;
step.V10 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V10, cospi[32], output.V11, cosBit);
step.V11 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V10, -cospi[32], output.V11, cosBit);
step.V12 = output.V12;
step.V13 = output.V13;
step.V14 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V14, cospi[32], output.V15, cosBit);
step.V15 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V14, -cospi[32], output.V15, cosBit);
// Stage 9 applies the AV1 signs and permutation that restore spatial sample order.
output.V0 = step.V0;
output.V1 = -step.V8;
output.V2 = step.V12;
output.V3 = -step.V4;
output.V4 = step.V6;
output.V5 = -step.V14;
output.V6 = step.V10;
output.V7 = -step.V2;
output.V8 = step.V3;
output.V9 = -step.V11;
output.V10 = step.V15;
output.V11 = -step.V7;
output.V12 = step.V5;
output.V13 = -step.V13;
output.V14 = step.V9;
output.V15 = -step.V1;
}
}
}

147
src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Adst4Operator.cs

@ -0,0 +1,147 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the four-point asymmetric discrete sine inverse transform operator.
/// </content>
internal static partial class Av1Inverse2dTransformer
{
/// <summary>
/// Defines the four-point AV1 inverse asymmetric discrete sine transform operator.
/// </summary>
/// <remarks>
/// Vector fields represent transform positions and vector lanes represent independent axes. The SIMD overloads apply
/// the same staged rotations, fixed-point rounding, and range clamps as the scalar overload without mixing axes.
/// </remarks>
internal readonly struct Adst4Operator : IAv1InverseTransform1dOperator
{
/// <summary>
/// Applies the normative four-point AV1 inverse asymmetric discrete sine transform.
/// </summary>
/// <param name="input">The four frequency-domain coefficients.</param>
/// <param name="output">The four spatial-domain residual values.</param>
/// <param name="step">The stage buffer owned by the containing two-dimensional transform.</param>
/// <param name="cosBit">The fixed-point precision of the sine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange)
{
ReadOnlySpan<int> sinpi = Av1SinusConstants.SinusPi(cosBit);
// libaom widens the complete four-point factorization because the products retain their fixed-point scale
// until the final shift. The stage buffer is therefore unnecessary for this transform size.
long x0 = input[0];
long x1 = input[1];
long x2 = input[2];
long x3 = input[3];
_ = step;
_ = stageRange;
// Avoid the multiplications for the all-zero coefficient vector, matching libaom's scalar kernel.
if ((x0 | x1 | x2 | x3) == 0)
{
output[..4].Clear();
return;
}
// Stages 1 and 2 form the seven sine products and the one unscaled combination used by stage 3.
long s0 = sinpi[1] * x0;
long s1 = sinpi[2] * x0;
long s2 = sinpi[3] * x1;
long s3 = sinpi[4] * x2;
long s4 = sinpi[1] * x2;
long s5 = sinpi[2] * x3;
long s6 = sinpi[4] * x3;
long s7 = (x0 - x2) + x3;
// Stages 3 through 6 combine the products while preserving the fixed-point scale until the final rounding.
s0 += s3;
s1 -= s4;
s3 = s2;
s2 = sinpi[3] * s7;
s0 += s5;
s1 -= s6;
x0 = s0 + s3;
x1 = s1 + s3;
x2 = s2;
x3 = (s0 + s1) - s3;
output[0] = Av1Math.RoundShift(x0, cosBit);
output[1] = Av1Math.RoundShift(x1, cosBit);
output[2] = Av1Math.RoundShift(x2, cosBit);
output[3] = Av1Math.RoundShift(x3, cosBit);
}
/// <inheritdoc/>
public static void Transform(
ref Av1TransformVector<Vector128<int>> input,
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
TransformCore(ref input, ref output, cosBit);
_ = step;
_ = stageRange;
}
/// <inheritdoc/>
public static void Transform(
ref Av1TransformVector<Vector256<int>> input,
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
TransformCore(ref input, ref output, cosBit);
_ = step;
_ = stageRange;
}
/// <summary>
/// Applies the inverse four-point matrix to four independent axes.
/// </summary>
/// <param name="input">The source values for four transform axes.</param>
/// <param name="output">The destination values for four transform axes.</param>
/// <param name="cosBit">The fixed-point precision of the sine constants.</param>
private static void TransformCore(ref Av1TransformVector<Vector128<int>> input, ref Av1TransformVector<Vector128<int>> output, int cosBit)
{
ReadOnlySpan<int> sinpi = Av1SinusConstants.SinusPi(cosBit);
Vector128<int> x0 = input.V0;
Vector128<int> x1 = input.V1;
Vector128<int> x2 = input.V2;
Vector128<int> x3 = input.V3;
// The products retain the sine-table scale across the complete matrix. The bounded transform inputs make
// the optimized kernels' wrapping 32-bit multiply/add sequence valid until the terminal rounding shift.
output.V0 = Av1Transform1dMath.MultiplyAdd4(sinpi[1], x0, sinpi[3], x1, sinpi[4], x2, sinpi[2], x3, cosBit);
output.V1 = Av1Transform1dMath.MultiplyAdd4(sinpi[2], x0, sinpi[3], x1, -sinpi[1], x2, -sinpi[4], x3, cosBit);
output.V2 = Av1Transform1dMath.MultiplyAdd4(sinpi[3], x0, 0, x1, -sinpi[3], x2, sinpi[3], x3, cosBit);
output.V3 = Av1Transform1dMath.MultiplyAdd4(sinpi[1] + sinpi[2], x0, -sinpi[3], x1, sinpi[4] - sinpi[1], x2, sinpi[2] - sinpi[4], x3, cosBit);
}
/// <summary>
/// Applies the inverse four-point matrix to eight independent axes.
/// </summary>
/// <param name="input">The source values for eight transform axes.</param>
/// <param name="output">The destination values for eight transform axes.</param>
/// <param name="cosBit">The fixed-point precision of the sine constants.</param>
private static void TransformCore(ref Av1TransformVector<Vector256<int>> input, ref Av1TransformVector<Vector256<int>> output, int cosBit)
{
ReadOnlySpan<int> sinpi = Av1SinusConstants.SinusPi(cosBit);
Vector256<int> x0 = input.V0;
Vector256<int> x1 = input.V1;
Vector256<int> x2 = input.V2;
Vector256<int> x3 = input.V3;
output.V0 = Av1Transform1dMath.MultiplyAdd4(sinpi[1], x0, sinpi[3], x1, sinpi[4], x2, sinpi[2], x3, cosBit);
output.V1 = Av1Transform1dMath.MultiplyAdd4(sinpi[2], x0, sinpi[3], x1, -sinpi[1], x2, -sinpi[4], x3, cosBit);
output.V2 = Av1Transform1dMath.MultiplyAdd4(sinpi[3], x0, 0, x1, -sinpi[3], x2, sinpi[3], x3, cosBit);
output.V3 = Av1Transform1dMath.MultiplyAdd4(sinpi[1] + sinpi[2], x0, -sinpi[3], x1, sinpi[4] - sinpi[1], x2, sinpi[2] - sinpi[4], x3, cosBit);
}
}
}

295
src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Adst8Operator.cs

@ -0,0 +1,295 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the eight-point asymmetric discrete sine inverse transform operator.
/// </content>
internal static partial class Av1Inverse2dTransformer
{
/// <summary>
/// Defines the eight-point AV1 inverse asymmetric discrete sine transform operator.
/// </summary>
/// <remarks>
/// Vector fields represent transform positions and vector lanes represent independent axes. The SIMD overloads apply
/// the same staged rotations, fixed-point rounding, and range clamps as the scalar overload without mixing axes.
/// </remarks>
internal readonly struct Adst8Operator : IAv1InverseTransform1dOperator
{
/// <summary>
/// Applies the normative eight-point AV1 inverse asymmetric discrete sine transform.
/// </summary>
/// <param name="input">The eight frequency-domain coefficients.</param>
/// <param name="output">The eight spatial-domain residual values.</param>
/// <param name="step">The eight-element stage buffer owned by the containing two-dimensional transform.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0;
// Stage 1 permutes the coefficients into the signed order used by the ADST factorization.
stage++;
output[0] = input[7];
output[1] = input[0];
output[2] = input[5];
output[3] = input[2];
output[4] = input[3];
output[5] = input[4];
output[6] = input[1];
output[7] = input[6];
// Stage 2 applies the terminal odd-angle rotations in reverse.
stage++;
step[0] = Av1Transform1dMath.HalfButterfly(cospi[4], output[0], cospi[60], output[1], cosBit);
step[1] = Av1Transform1dMath.HalfButterfly(cospi[60], output[0], -cospi[4], output[1], cosBit);
step[2] = Av1Transform1dMath.HalfButterfly(cospi[20], output[2], cospi[44], output[3], cosBit);
step[3] = Av1Transform1dMath.HalfButterfly(cospi[44], output[2], -cospi[20], output[3], cosBit);
step[4] = Av1Transform1dMath.HalfButterfly(cospi[36], output[4], cospi[28], output[5], cosBit);
step[5] = Av1Transform1dMath.HalfButterfly(cospi[28], output[4], -cospi[36], output[5], cosBit);
step[6] = Av1Transform1dMath.HalfButterfly(cospi[52], output[6], cospi[12], output[7], cosBit);
step[7] = Av1Transform1dMath.HalfButterfly(cospi[12], output[6], -cospi[52], output[7], cosBit);
// Stage 3 separates the complete butterfly into two four-sample halves and clamps each lane.
stage++;
output[0] = Av1Transform1dMath.Clamp(step[0] + step[4], stageRange[stage]);
output[1] = Av1Transform1dMath.Clamp(step[1] + step[5], stageRange[stage]);
output[2] = Av1Transform1dMath.Clamp(step[2] + step[6], stageRange[stage]);
output[3] = Av1Transform1dMath.Clamp(step[3] + step[7], stageRange[stage]);
output[4] = Av1Transform1dMath.Clamp(step[0] - step[4], stageRange[stage]);
output[5] = Av1Transform1dMath.Clamp(step[1] - step[5], stageRange[stage]);
output[6] = Av1Transform1dMath.Clamp(step[2] - step[6], stageRange[stage]);
output[7] = Av1Transform1dMath.Clamp(step[3] - step[7], stageRange[stage]);
// Stage 4 reverses the pi/8 and 3pi/8 rotations in the upper half.
stage++;
step[0] = output[0];
step[1] = output[1];
step[2] = output[2];
step[3] = output[3];
step[4] = Av1Transform1dMath.HalfButterfly(cospi[16], output[4], cospi[48], output[5], cosBit);
step[5] = Av1Transform1dMath.HalfButterfly(cospi[48], output[4], -cospi[16], output[5], cosBit);
step[6] = Av1Transform1dMath.HalfButterfly(-cospi[48], output[6], cospi[16], output[7], cosBit);
step[7] = Av1Transform1dMath.HalfButterfly(cospi[16], output[6], cospi[48], output[7], cosBit);
// Stage 5 separates the four-sample halves into adjacent coefficient pairs and clamps each lane.
stage++;
output[0] = Av1Transform1dMath.Clamp(step[0] + step[2], stageRange[stage]);
output[1] = Av1Transform1dMath.Clamp(step[1] + step[3], stageRange[stage]);
output[2] = Av1Transform1dMath.Clamp(step[0] - step[2], stageRange[stage]);
output[3] = Av1Transform1dMath.Clamp(step[1] - step[3], stageRange[stage]);
output[4] = Av1Transform1dMath.Clamp(step[4] + step[6], stageRange[stage]);
output[5] = Av1Transform1dMath.Clamp(step[5] + step[7], stageRange[stage]);
output[6] = Av1Transform1dMath.Clamp(step[4] - step[6], stageRange[stage]);
output[7] = Av1Transform1dMath.Clamp(step[5] - step[7], stageRange[stage]);
// Stage 6 reverses the pi/4 rotations for the middle pairs.
stage++;
step[0] = output[0];
step[1] = output[1];
step[2] = Av1Transform1dMath.HalfButterfly(cospi[32], output[2], cospi[32], output[3], cosBit);
step[3] = Av1Transform1dMath.HalfButterfly(cospi[32], output[2], -cospi[32], output[3], cosBit);
step[4] = output[4];
step[5] = output[5];
step[6] = Av1Transform1dMath.HalfButterfly(cospi[32], output[6], cospi[32], output[7], cosBit);
step[7] = Av1Transform1dMath.HalfButterfly(cospi[32], output[6], -cospi[32], output[7], cosBit);
// Stage 7 applies the AV1 signs and permutation that restore spatial sample order.
output[0] = step[0];
output[1] = -step[4];
output[2] = step[6];
output[3] = -step[2];
output[4] = step[3];
output[5] = -step[7];
output[6] = step[5];
output[7] = -step[1];
}
/// <inheritdoc/>
public static void Transform(
ref Av1TransformVector<Vector256<int>> input,
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0;
// Stage 1 permutes the coefficients into the signed order used by the ADST factorization.
stage++;
output.V0 = input.V7;
output.V1 = input.V0;
output.V2 = input.V5;
output.V3 = input.V2;
output.V4 = input.V3;
output.V5 = input.V4;
output.V6 = input.V1;
output.V7 = input.V6;
// Stage 2 applies the terminal odd-angle rotations in reverse.
stage++;
step.V0 = Av1Transform1dMath.HalfButterfly(cospi[4], output.V0, cospi[60], output.V1, cosBit);
step.V1 = Av1Transform1dMath.HalfButterfly(cospi[60], output.V0, -cospi[4], output.V1, cosBit);
step.V2 = Av1Transform1dMath.HalfButterfly(cospi[20], output.V2, cospi[44], output.V3, cosBit);
step.V3 = Av1Transform1dMath.HalfButterfly(cospi[44], output.V2, -cospi[20], output.V3, cosBit);
step.V4 = Av1Transform1dMath.HalfButterfly(cospi[36], output.V4, cospi[28], output.V5, cosBit);
step.V5 = Av1Transform1dMath.HalfButterfly(cospi[28], output.V4, -cospi[36], output.V5, cosBit);
step.V6 = Av1Transform1dMath.HalfButterfly(cospi[52], output.V6, cospi[12], output.V7, cosBit);
step.V7 = Av1Transform1dMath.HalfButterfly(cospi[12], output.V6, -cospi[52], output.V7, cosBit);
// Stage 3 separates the complete butterfly into two four-sample halves and clamps each lane.
stage++;
output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V4, stageRange[stage]);
output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V5, stageRange[stage]);
output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V6, stageRange[stage]);
output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V7, stageRange[stage]);
output.V4 = Av1Transform1dMath.Clamp(step.V0 - step.V4, stageRange[stage]);
output.V5 = Av1Transform1dMath.Clamp(step.V1 - step.V5, stageRange[stage]);
output.V6 = Av1Transform1dMath.Clamp(step.V2 - step.V6, stageRange[stage]);
output.V7 = Av1Transform1dMath.Clamp(step.V3 - step.V7, stageRange[stage]);
// Stage 4 reverses the pi/8 and 3pi/8 rotations in the upper half.
stage++;
step.V0 = output.V0;
step.V1 = output.V1;
step.V2 = output.V2;
step.V3 = output.V3;
step.V4 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V4, cospi[48], output.V5, cosBit);
step.V5 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V4, -cospi[16], output.V5, cosBit);
step.V6 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V6, cospi[16], output.V7, cosBit);
step.V7 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V6, cospi[48], output.V7, cosBit);
// Stage 5 separates the four-sample halves into adjacent coefficient pairs and clamps each lane.
stage++;
output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V2, stageRange[stage]);
output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V3, stageRange[stage]);
output.V2 = Av1Transform1dMath.Clamp(step.V0 - step.V2, stageRange[stage]);
output.V3 = Av1Transform1dMath.Clamp(step.V1 - step.V3, stageRange[stage]);
output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V6, stageRange[stage]);
output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V7, stageRange[stage]);
output.V6 = Av1Transform1dMath.Clamp(step.V4 - step.V6, stageRange[stage]);
output.V7 = Av1Transform1dMath.Clamp(step.V5 - step.V7, stageRange[stage]);
// Stage 6 reverses the pi/4 rotations for the middle pairs.
stage++;
step.V0 = output.V0;
step.V1 = output.V1;
step.V2 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V2, cospi[32], output.V3, cosBit);
step.V3 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V2, -cospi[32], output.V3, cosBit);
step.V4 = output.V4;
step.V5 = output.V5;
step.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V6, cospi[32], output.V7, cosBit);
step.V7 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V6, -cospi[32], output.V7, cosBit);
// Stage 7 applies the AV1 signs and permutation that restore spatial sample order.
output.V0 = step.V0;
output.V1 = -step.V4;
output.V2 = step.V6;
output.V3 = -step.V2;
output.V4 = step.V3;
output.V5 = -step.V7;
output.V6 = step.V5;
output.V7 = -step.V1;
}
/// <summary>
/// Applies the transform to four independent axes in parallel.
/// </summary>
/// <param name="input">The source values for the parallel transform axes.</param>
/// <param name="output">The destination values for the parallel transform axes.</param>
/// <param name="step">The fixed stage storage for the parallel transform axes.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(
ref Av1TransformVector<Vector128<int>> input,
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0;
// Stage 1 permutes the coefficients into the signed order used by the ADST factorization.
stage++;
output.V0 = input.V7;
output.V1 = input.V0;
output.V2 = input.V5;
output.V3 = input.V2;
output.V4 = input.V3;
output.V5 = input.V4;
output.V6 = input.V1;
output.V7 = input.V6;
// Stage 2 applies the terminal odd-angle rotations in reverse.
stage++;
step.V0 = Av1Transform1dMath.HalfButterfly(cospi[4], output.V0, cospi[60], output.V1, cosBit);
step.V1 = Av1Transform1dMath.HalfButterfly(cospi[60], output.V0, -cospi[4], output.V1, cosBit);
step.V2 = Av1Transform1dMath.HalfButterfly(cospi[20], output.V2, cospi[44], output.V3, cosBit);
step.V3 = Av1Transform1dMath.HalfButterfly(cospi[44], output.V2, -cospi[20], output.V3, cosBit);
step.V4 = Av1Transform1dMath.HalfButterfly(cospi[36], output.V4, cospi[28], output.V5, cosBit);
step.V5 = Av1Transform1dMath.HalfButterfly(cospi[28], output.V4, -cospi[36], output.V5, cosBit);
step.V6 = Av1Transform1dMath.HalfButterfly(cospi[52], output.V6, cospi[12], output.V7, cosBit);
step.V7 = Av1Transform1dMath.HalfButterfly(cospi[12], output.V6, -cospi[52], output.V7, cosBit);
// Stage 3 separates the complete butterfly into two four-sample halves and clamps each lane.
stage++;
output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V4, stageRange[stage]);
output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V5, stageRange[stage]);
output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V6, stageRange[stage]);
output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V7, stageRange[stage]);
output.V4 = Av1Transform1dMath.Clamp(step.V0 - step.V4, stageRange[stage]);
output.V5 = Av1Transform1dMath.Clamp(step.V1 - step.V5, stageRange[stage]);
output.V6 = Av1Transform1dMath.Clamp(step.V2 - step.V6, stageRange[stage]);
output.V7 = Av1Transform1dMath.Clamp(step.V3 - step.V7, stageRange[stage]);
// Stage 4 reverses the pi/8 and 3pi/8 rotations in the upper half.
stage++;
step.V0 = output.V0;
step.V1 = output.V1;
step.V2 = output.V2;
step.V3 = output.V3;
step.V4 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V4, cospi[48], output.V5, cosBit);
step.V5 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V4, -cospi[16], output.V5, cosBit);
step.V6 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V6, cospi[16], output.V7, cosBit);
step.V7 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V6, cospi[48], output.V7, cosBit);
// Stage 5 separates the four-sample halves into adjacent coefficient pairs and clamps each lane.
stage++;
output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V2, stageRange[stage]);
output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V3, stageRange[stage]);
output.V2 = Av1Transform1dMath.Clamp(step.V0 - step.V2, stageRange[stage]);
output.V3 = Av1Transform1dMath.Clamp(step.V1 - step.V3, stageRange[stage]);
output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V6, stageRange[stage]);
output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V7, stageRange[stage]);
output.V6 = Av1Transform1dMath.Clamp(step.V4 - step.V6, stageRange[stage]);
output.V7 = Av1Transform1dMath.Clamp(step.V5 - step.V7, stageRange[stage]);
// Stage 6 reverses the pi/4 rotations for the middle pairs.
stage++;
step.V0 = output.V0;
step.V1 = output.V1;
step.V2 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V2, cospi[32], output.V3, cosBit);
step.V3 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V2, -cospi[32], output.V3, cosBit);
step.V4 = output.V4;
step.V5 = output.V5;
step.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V6, cospi[32], output.V7, cosBit);
step.V7 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V6, -cospi[32], output.V7, cosBit);
// Stage 7 applies the AV1 signs and permutation that restore spatial sample order.
output.V0 = step.V0;
output.V1 = -step.V4;
output.V2 = step.V6;
output.V3 = -step.V2;
output.V4 = step.V3;
output.V5 = -step.V7;
output.V6 = step.V5;
output.V7 = -step.V1;
}
}
}

481
src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Dct16Operator.cs

@ -0,0 +1,481 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the sixteen-point discrete cosine inverse transform operator.
/// </content>
internal static partial class Av1Inverse2dTransformer
{
/// <summary>
/// Defines the 16-point AV1 inverse discrete cosine transform operator.
/// </summary>
/// <remarks>
/// Vector fields represent transform positions and vector lanes represent independent axes. The SIMD overloads apply
/// the same staged butterflies, fixed-point rounding, and range clamps as the scalar overload without mixing axes.
/// </remarks>
internal readonly struct Dct16Operator : IAv1InverseTransform1dOperator
{
/// <summary>
/// Applies the normative 16-point AV1 inverse discrete cosine transform.
/// </summary>
/// <param name="input">The sixteen frequency-domain coefficients.</param>
/// <param name="output">The sixteen spatial-domain residual values.</param>
/// <param name="step">The sixteen-element stage buffer owned by the containing two-dimensional transform.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0;
// Stage 1 permutes frequency-ordered coefficients into the recursive DCT factorization order.
stage++;
output[0] = input[0];
output[1] = input[8];
output[2] = input[4];
output[3] = input[12];
output[4] = input[2];
output[5] = input[10];
output[6] = input[6];
output[7] = input[14];
output[8] = input[1];
output[9] = input[9];
output[10] = input[5];
output[11] = input[13];
output[12] = input[3];
output[13] = input[11];
output[14] = input[7];
output[15] = input[15];
// Stage 2 rotates the highest odd-frequency coefficient pairs by their pi/32 angles.
stage++;
step[0] = output[0];
step[1] = output[1];
step[2] = output[2];
step[3] = output[3];
step[4] = output[4];
step[5] = output[5];
step[6] = output[6];
step[7] = output[7];
step[8] = Av1Transform1dMath.HalfButterfly(cospi[60], output[8], -cospi[4], output[15], cosBit);
step[9] = Av1Transform1dMath.HalfButterfly(cospi[28], output[9], -cospi[36], output[14], cosBit);
step[10] = Av1Transform1dMath.HalfButterfly(cospi[44], output[10], -cospi[20], output[13], cosBit);
step[11] = Av1Transform1dMath.HalfButterfly(cospi[12], output[11], -cospi[52], output[12], cosBit);
step[12] = Av1Transform1dMath.HalfButterfly(cospi[52], output[11], cospi[12], output[12], cosBit);
step[13] = Av1Transform1dMath.HalfButterfly(cospi[20], output[10], cospi[44], output[13], cosBit);
step[14] = Av1Transform1dMath.HalfButterfly(cospi[36], output[9], cospi[28], output[14], cosBit);
step[15] = Av1Transform1dMath.HalfButterfly(cospi[4], output[8], cospi[60], output[15], cosBit);
// Stage 3 reconstructs the embedded eight-point groups and combines adjacent odd terms.
stage++;
byte range = stageRange[stage];
output[0] = step[0];
output[1] = step[1];
output[2] = step[2];
output[3] = step[3];
output[4] = Av1Transform1dMath.HalfButterfly(cospi[56], step[4], -cospi[8], step[7], cosBit);
output[5] = Av1Transform1dMath.HalfButterfly(cospi[24], step[5], -cospi[40], step[6], cosBit);
output[6] = Av1Transform1dMath.HalfButterfly(cospi[40], step[5], cospi[24], step[6], cosBit);
output[7] = Av1Transform1dMath.HalfButterfly(cospi[8], step[4], cospi[56], step[7], cosBit);
output[8] = Av1Transform1dMath.Clamp(step[8] + step[9], range);
output[9] = Av1Transform1dMath.Clamp(step[8] - step[9], range);
output[10] = Av1Transform1dMath.Clamp(step[11] - step[10], range);
output[11] = Av1Transform1dMath.Clamp(step[10] + step[11], range);
output[12] = Av1Transform1dMath.Clamp(step[12] + step[13], range);
output[13] = Av1Transform1dMath.Clamp(step[12] - step[13], range);
output[14] = Av1Transform1dMath.Clamp(step[15] - step[14], range);
output[15] = Av1Transform1dMath.Clamp(step[14] + step[15], range);
// Stage 4 completes the low-frequency four-point DCT and rotates the next odd-frequency pairs.
stage++;
range = stageRange[stage];
step[0] = Av1Transform1dMath.HalfButterfly(cospi[32], output[0], cospi[32], output[1], cosBit);
step[1] = Av1Transform1dMath.HalfButterfly(cospi[32], output[0], -cospi[32], output[1], cosBit);
step[2] = Av1Transform1dMath.HalfButterfly(cospi[48], output[2], -cospi[16], output[3], cosBit);
step[3] = Av1Transform1dMath.HalfButterfly(cospi[16], output[2], cospi[48], output[3], cosBit);
step[4] = Av1Transform1dMath.Clamp(output[4] + output[5], range);
step[5] = Av1Transform1dMath.Clamp(output[4] - output[5], range);
step[6] = Av1Transform1dMath.Clamp(output[7] - output[6], range);
step[7] = Av1Transform1dMath.Clamp(output[6] + output[7], range);
step[8] = output[8];
step[9] = Av1Transform1dMath.HalfButterfly(-cospi[16], output[9], cospi[48], output[14], cosBit);
step[10] = Av1Transform1dMath.HalfButterfly(-cospi[48], output[10], -cospi[16], output[13], cosBit);
step[11] = output[11];
step[12] = output[12];
step[13] = Av1Transform1dMath.HalfButterfly(-cospi[16], output[10], cospi[48], output[13], cosBit);
step[14] = Av1Transform1dMath.HalfButterfly(cospi[48], output[9], cospi[16], output[14], cosBit);
step[15] = output[15];
// Stage 5 widens the reconstructed groups through their next butterfly level.
stage++;
range = stageRange[stage];
output[0] = Av1Transform1dMath.Clamp(step[0] + step[3], range);
output[1] = Av1Transform1dMath.Clamp(step[1] + step[2], range);
output[2] = Av1Transform1dMath.Clamp(step[1] - step[2], range);
output[3] = Av1Transform1dMath.Clamp(step[0] - step[3], range);
output[4] = step[4];
output[5] = Av1Transform1dMath.HalfButterfly(-cospi[32], step[5], cospi[32], step[6], cosBit);
output[6] = Av1Transform1dMath.HalfButterfly(cospi[32], step[5], cospi[32], step[6], cosBit);
output[7] = step[7];
output[8] = Av1Transform1dMath.Clamp(step[8] + step[11], range);
output[9] = Av1Transform1dMath.Clamp(step[9] + step[10], range);
output[10] = Av1Transform1dMath.Clamp(step[9] - step[10], range);
output[11] = Av1Transform1dMath.Clamp(step[8] - step[11], range);
output[12] = Av1Transform1dMath.Clamp(step[15] - step[12], range);
output[13] = Av1Transform1dMath.Clamp(step[14] - step[13], range);
output[14] = Av1Transform1dMath.Clamp(step[13] + step[14], range);
output[15] = Av1Transform1dMath.Clamp(step[12] + step[15], range);
// Stage 6 applies the remaining pi/4 rotations before the terminal spatial merge.
stage++;
range = stageRange[stage];
step[0] = Av1Transform1dMath.Clamp(output[0] + output[7], range);
step[1] = Av1Transform1dMath.Clamp(output[1] + output[6], range);
step[2] = Av1Transform1dMath.Clamp(output[2] + output[5], range);
step[3] = Av1Transform1dMath.Clamp(output[3] + output[4], range);
step[4] = Av1Transform1dMath.Clamp(output[3] - output[4], range);
step[5] = Av1Transform1dMath.Clamp(output[2] - output[5], range);
step[6] = Av1Transform1dMath.Clamp(output[1] - output[6], range);
step[7] = Av1Transform1dMath.Clamp(output[0] - output[7], range);
step[8] = output[8];
step[9] = output[9];
step[10] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[10], cospi[32], output[13], cosBit);
step[11] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[11], cospi[32], output[12], cosBit);
step[12] = Av1Transform1dMath.HalfButterfly(cospi[32], output[11], cospi[32], output[12], cosBit);
step[13] = Av1Transform1dMath.HalfButterfly(cospi[32], output[10], cospi[32], output[13], cosBit);
step[14] = output[14];
step[15] = output[15];
// Stage 7 merges the even and odd halves into spatial order and clamps every result.
stage++;
range = stageRange[stage];
output[0] = Av1Transform1dMath.Clamp(step[0] + step[15], range);
output[1] = Av1Transform1dMath.Clamp(step[1] + step[14], range);
output[2] = Av1Transform1dMath.Clamp(step[2] + step[13], range);
output[3] = Av1Transform1dMath.Clamp(step[3] + step[12], range);
output[4] = Av1Transform1dMath.Clamp(step[4] + step[11], range);
output[5] = Av1Transform1dMath.Clamp(step[5] + step[10], range);
output[6] = Av1Transform1dMath.Clamp(step[6] + step[9], range);
output[7] = Av1Transform1dMath.Clamp(step[7] + step[8], range);
output[8] = Av1Transform1dMath.Clamp(step[7] - step[8], range);
output[9] = Av1Transform1dMath.Clamp(step[6] - step[9], range);
output[10] = Av1Transform1dMath.Clamp(step[5] - step[10], range);
output[11] = Av1Transform1dMath.Clamp(step[4] - step[11], range);
output[12] = Av1Transform1dMath.Clamp(step[3] - step[12], range);
output[13] = Av1Transform1dMath.Clamp(step[2] - step[13], range);
output[14] = Av1Transform1dMath.Clamp(step[1] - step[14], range);
output[15] = Av1Transform1dMath.Clamp(step[0] - step[15], range);
}
/// <inheritdoc/>
public static void Transform(
ref Av1TransformVector<Vector256<int>> input,
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0;
// Stage 1 permutes frequency-ordered coefficients into the recursive DCT factorization order.
stage++;
output.V0 = input.V0;
output.V1 = input.V8;
output.V2 = input.V4;
output.V3 = input.V12;
output.V4 = input.V2;
output.V5 = input.V10;
output.V6 = input.V6;
output.V7 = input.V14;
output.V8 = input.V1;
output.V9 = input.V9;
output.V10 = input.V5;
output.V11 = input.V13;
output.V12 = input.V3;
output.V13 = input.V11;
output.V14 = input.V7;
output.V15 = input.V15;
// Stage 2 rotates the highest odd-frequency coefficient pairs by their pi/32 angles.
stage++;
step.V0 = output.V0;
step.V1 = output.V1;
step.V2 = output.V2;
step.V3 = output.V3;
step.V4 = output.V4;
step.V5 = output.V5;
step.V6 = output.V6;
step.V7 = output.V7;
step.V8 = Av1Transform1dMath.HalfButterfly(cospi[60], output.V8, -cospi[4], output.V15, cosBit);
step.V9 = Av1Transform1dMath.HalfButterfly(cospi[28], output.V9, -cospi[36], output.V14, cosBit);
step.V10 = Av1Transform1dMath.HalfButterfly(cospi[44], output.V10, -cospi[20], output.V13, cosBit);
step.V11 = Av1Transform1dMath.HalfButterfly(cospi[12], output.V11, -cospi[52], output.V12, cosBit);
step.V12 = Av1Transform1dMath.HalfButterfly(cospi[52], output.V11, cospi[12], output.V12, cosBit);
step.V13 = Av1Transform1dMath.HalfButterfly(cospi[20], output.V10, cospi[44], output.V13, cosBit);
step.V14 = Av1Transform1dMath.HalfButterfly(cospi[36], output.V9, cospi[28], output.V14, cosBit);
step.V15 = Av1Transform1dMath.HalfButterfly(cospi[4], output.V8, cospi[60], output.V15, cosBit);
// Stage 3 reconstructs the embedded eight-point groups and combines adjacent odd terms.
stage++;
byte range = stageRange[stage];
output.V0 = step.V0;
output.V1 = step.V1;
output.V2 = step.V2;
output.V3 = step.V3;
output.V4 = Av1Transform1dMath.HalfButterfly(cospi[56], step.V4, -cospi[8], step.V7, cosBit);
output.V5 = Av1Transform1dMath.HalfButterfly(cospi[24], step.V5, -cospi[40], step.V6, cosBit);
output.V6 = Av1Transform1dMath.HalfButterfly(cospi[40], step.V5, cospi[24], step.V6, cosBit);
output.V7 = Av1Transform1dMath.HalfButterfly(cospi[8], step.V4, cospi[56], step.V7, cosBit);
output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V9, range);
output.V9 = Av1Transform1dMath.Clamp(step.V8 - step.V9, range);
output.V10 = Av1Transform1dMath.Clamp(step.V11 - step.V10, range);
output.V11 = Av1Transform1dMath.Clamp(step.V10 + step.V11, range);
output.V12 = Av1Transform1dMath.Clamp(step.V12 + step.V13, range);
output.V13 = Av1Transform1dMath.Clamp(step.V12 - step.V13, range);
output.V14 = Av1Transform1dMath.Clamp(step.V15 - step.V14, range);
output.V15 = Av1Transform1dMath.Clamp(step.V14 + step.V15, range);
// Stage 4 completes the low-frequency four-point DCT and rotates the next odd-frequency pairs.
stage++;
range = stageRange[stage];
step.V0 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, cospi[32], output.V1, cosBit);
step.V1 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, -cospi[32], output.V1, cosBit);
step.V2 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V2, -cospi[16], output.V3, cosBit);
step.V3 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V2, cospi[48], output.V3, cosBit);
step.V4 = Av1Transform1dMath.Clamp(output.V4 + output.V5, range);
step.V5 = Av1Transform1dMath.Clamp(output.V4 - output.V5, range);
step.V6 = Av1Transform1dMath.Clamp(output.V7 - output.V6, range);
step.V7 = Av1Transform1dMath.Clamp(output.V6 + output.V7, range);
step.V8 = output.V8;
step.V9 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V9, cospi[48], output.V14, cosBit);
step.V10 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V10, -cospi[16], output.V13, cosBit);
step.V11 = output.V11;
step.V12 = output.V12;
step.V13 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V10, cospi[48], output.V13, cosBit);
step.V14 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V9, cospi[16], output.V14, cosBit);
step.V15 = output.V15;
// Stage 5 widens the reconstructed groups through their next butterfly level.
stage++;
range = stageRange[stage];
output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V3, range);
output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V2, range);
output.V2 = Av1Transform1dMath.Clamp(step.V1 - step.V2, range);
output.V3 = Av1Transform1dMath.Clamp(step.V0 - step.V3, range);
output.V4 = step.V4;
output.V5 = Av1Transform1dMath.HalfButterfly(-cospi[32], step.V5, cospi[32], step.V6, cosBit);
output.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V5, cospi[32], step.V6, cosBit);
output.V7 = step.V7;
output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V11, range);
output.V9 = Av1Transform1dMath.Clamp(step.V9 + step.V10, range);
output.V10 = Av1Transform1dMath.Clamp(step.V9 - step.V10, range);
output.V11 = Av1Transform1dMath.Clamp(step.V8 - step.V11, range);
output.V12 = Av1Transform1dMath.Clamp(step.V15 - step.V12, range);
output.V13 = Av1Transform1dMath.Clamp(step.V14 - step.V13, range);
output.V14 = Av1Transform1dMath.Clamp(step.V13 + step.V14, range);
output.V15 = Av1Transform1dMath.Clamp(step.V12 + step.V15, range);
// Stage 6 applies the remaining pi/4 rotations before the terminal spatial merge.
stage++;
range = stageRange[stage];
step.V0 = Av1Transform1dMath.Clamp(output.V0 + output.V7, range);
step.V1 = Av1Transform1dMath.Clamp(output.V1 + output.V6, range);
step.V2 = Av1Transform1dMath.Clamp(output.V2 + output.V5, range);
step.V3 = Av1Transform1dMath.Clamp(output.V3 + output.V4, range);
step.V4 = Av1Transform1dMath.Clamp(output.V3 - output.V4, range);
step.V5 = Av1Transform1dMath.Clamp(output.V2 - output.V5, range);
step.V6 = Av1Transform1dMath.Clamp(output.V1 - output.V6, range);
step.V7 = Av1Transform1dMath.Clamp(output.V0 - output.V7, range);
step.V8 = output.V8;
step.V9 = output.V9;
step.V10 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V10, cospi[32], output.V13, cosBit);
step.V11 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V11, cospi[32], output.V12, cosBit);
step.V12 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V11, cospi[32], output.V12, cosBit);
step.V13 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V10, cospi[32], output.V13, cosBit);
step.V14 = output.V14;
step.V15 = output.V15;
// Stage 7 merges the even and odd halves into spatial order and clamps every result.
stage++;
range = stageRange[stage];
output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V15, range);
output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V14, range);
output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V13, range);
output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V12, range);
output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V11, range);
output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V10, range);
output.V6 = Av1Transform1dMath.Clamp(step.V6 + step.V9, range);
output.V7 = Av1Transform1dMath.Clamp(step.V7 + step.V8, range);
output.V8 = Av1Transform1dMath.Clamp(step.V7 - step.V8, range);
output.V9 = Av1Transform1dMath.Clamp(step.V6 - step.V9, range);
output.V10 = Av1Transform1dMath.Clamp(step.V5 - step.V10, range);
output.V11 = Av1Transform1dMath.Clamp(step.V4 - step.V11, range);
output.V12 = Av1Transform1dMath.Clamp(step.V3 - step.V12, range);
output.V13 = Av1Transform1dMath.Clamp(step.V2 - step.V13, range);
output.V14 = Av1Transform1dMath.Clamp(step.V1 - step.V14, range);
output.V15 = Av1Transform1dMath.Clamp(step.V0 - step.V15, range);
}
/// <summary>
/// Applies the transform to four independent axes in parallel.
/// </summary>
/// <param name="input">The source values for the parallel transform axes.</param>
/// <param name="output">The destination values for the parallel transform axes.</param>
/// <param name="step">The fixed stage storage for the parallel transform axes.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(
ref Av1TransformVector<Vector128<int>> input,
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0;
// Stage 1 permutes frequency-ordered coefficients into the recursive DCT factorization order.
stage++;
output.V0 = input.V0;
output.V1 = input.V8;
output.V2 = input.V4;
output.V3 = input.V12;
output.V4 = input.V2;
output.V5 = input.V10;
output.V6 = input.V6;
output.V7 = input.V14;
output.V8 = input.V1;
output.V9 = input.V9;
output.V10 = input.V5;
output.V11 = input.V13;
output.V12 = input.V3;
output.V13 = input.V11;
output.V14 = input.V7;
output.V15 = input.V15;
// Stage 2 rotates the highest odd-frequency coefficient pairs by their pi/32 angles.
stage++;
step.V0 = output.V0;
step.V1 = output.V1;
step.V2 = output.V2;
step.V3 = output.V3;
step.V4 = output.V4;
step.V5 = output.V5;
step.V6 = output.V6;
step.V7 = output.V7;
step.V8 = Av1Transform1dMath.HalfButterfly(cospi[60], output.V8, -cospi[4], output.V15, cosBit);
step.V9 = Av1Transform1dMath.HalfButterfly(cospi[28], output.V9, -cospi[36], output.V14, cosBit);
step.V10 = Av1Transform1dMath.HalfButterfly(cospi[44], output.V10, -cospi[20], output.V13, cosBit);
step.V11 = Av1Transform1dMath.HalfButterfly(cospi[12], output.V11, -cospi[52], output.V12, cosBit);
step.V12 = Av1Transform1dMath.HalfButterfly(cospi[52], output.V11, cospi[12], output.V12, cosBit);
step.V13 = Av1Transform1dMath.HalfButterfly(cospi[20], output.V10, cospi[44], output.V13, cosBit);
step.V14 = Av1Transform1dMath.HalfButterfly(cospi[36], output.V9, cospi[28], output.V14, cosBit);
step.V15 = Av1Transform1dMath.HalfButterfly(cospi[4], output.V8, cospi[60], output.V15, cosBit);
// Stage 3 reconstructs the embedded eight-point groups and combines adjacent odd terms.
stage++;
byte range = stageRange[stage];
output.V0 = step.V0;
output.V1 = step.V1;
output.V2 = step.V2;
output.V3 = step.V3;
output.V4 = Av1Transform1dMath.HalfButterfly(cospi[56], step.V4, -cospi[8], step.V7, cosBit);
output.V5 = Av1Transform1dMath.HalfButterfly(cospi[24], step.V5, -cospi[40], step.V6, cosBit);
output.V6 = Av1Transform1dMath.HalfButterfly(cospi[40], step.V5, cospi[24], step.V6, cosBit);
output.V7 = Av1Transform1dMath.HalfButterfly(cospi[8], step.V4, cospi[56], step.V7, cosBit);
output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V9, range);
output.V9 = Av1Transform1dMath.Clamp(step.V8 - step.V9, range);
output.V10 = Av1Transform1dMath.Clamp(step.V11 - step.V10, range);
output.V11 = Av1Transform1dMath.Clamp(step.V10 + step.V11, range);
output.V12 = Av1Transform1dMath.Clamp(step.V12 + step.V13, range);
output.V13 = Av1Transform1dMath.Clamp(step.V12 - step.V13, range);
output.V14 = Av1Transform1dMath.Clamp(step.V15 - step.V14, range);
output.V15 = Av1Transform1dMath.Clamp(step.V14 + step.V15, range);
// Stage 4 completes the low-frequency four-point DCT and rotates the next odd-frequency pairs.
stage++;
range = stageRange[stage];
step.V0 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, cospi[32], output.V1, cosBit);
step.V1 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, -cospi[32], output.V1, cosBit);
step.V2 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V2, -cospi[16], output.V3, cosBit);
step.V3 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V2, cospi[48], output.V3, cosBit);
step.V4 = Av1Transform1dMath.Clamp(output.V4 + output.V5, range);
step.V5 = Av1Transform1dMath.Clamp(output.V4 - output.V5, range);
step.V6 = Av1Transform1dMath.Clamp(output.V7 - output.V6, range);
step.V7 = Av1Transform1dMath.Clamp(output.V6 + output.V7, range);
step.V8 = output.V8;
step.V9 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V9, cospi[48], output.V14, cosBit);
step.V10 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V10, -cospi[16], output.V13, cosBit);
step.V11 = output.V11;
step.V12 = output.V12;
step.V13 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V10, cospi[48], output.V13, cosBit);
step.V14 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V9, cospi[16], output.V14, cosBit);
step.V15 = output.V15;
// Stage 5 widens the reconstructed groups through their next butterfly level.
stage++;
range = stageRange[stage];
output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V3, range);
output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V2, range);
output.V2 = Av1Transform1dMath.Clamp(step.V1 - step.V2, range);
output.V3 = Av1Transform1dMath.Clamp(step.V0 - step.V3, range);
output.V4 = step.V4;
output.V5 = Av1Transform1dMath.HalfButterfly(-cospi[32], step.V5, cospi[32], step.V6, cosBit);
output.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V5, cospi[32], step.V6, cosBit);
output.V7 = step.V7;
output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V11, range);
output.V9 = Av1Transform1dMath.Clamp(step.V9 + step.V10, range);
output.V10 = Av1Transform1dMath.Clamp(step.V9 - step.V10, range);
output.V11 = Av1Transform1dMath.Clamp(step.V8 - step.V11, range);
output.V12 = Av1Transform1dMath.Clamp(step.V15 - step.V12, range);
output.V13 = Av1Transform1dMath.Clamp(step.V14 - step.V13, range);
output.V14 = Av1Transform1dMath.Clamp(step.V13 + step.V14, range);
output.V15 = Av1Transform1dMath.Clamp(step.V12 + step.V15, range);
// Stage 6 applies the remaining pi/4 rotations before the terminal spatial merge.
stage++;
range = stageRange[stage];
step.V0 = Av1Transform1dMath.Clamp(output.V0 + output.V7, range);
step.V1 = Av1Transform1dMath.Clamp(output.V1 + output.V6, range);
step.V2 = Av1Transform1dMath.Clamp(output.V2 + output.V5, range);
step.V3 = Av1Transform1dMath.Clamp(output.V3 + output.V4, range);
step.V4 = Av1Transform1dMath.Clamp(output.V3 - output.V4, range);
step.V5 = Av1Transform1dMath.Clamp(output.V2 - output.V5, range);
step.V6 = Av1Transform1dMath.Clamp(output.V1 - output.V6, range);
step.V7 = Av1Transform1dMath.Clamp(output.V0 - output.V7, range);
step.V8 = output.V8;
step.V9 = output.V9;
step.V10 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V10, cospi[32], output.V13, cosBit);
step.V11 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V11, cospi[32], output.V12, cosBit);
step.V12 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V11, cospi[32], output.V12, cosBit);
step.V13 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V10, cospi[32], output.V13, cosBit);
step.V14 = output.V14;
step.V15 = output.V15;
// Stage 7 merges the even and odd halves into spatial order and clamps every result.
stage++;
range = stageRange[stage];
output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V15, range);
output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V14, range);
output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V13, range);
output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V12, range);
output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V11, range);
output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V10, range);
output.V6 = Av1Transform1dMath.Clamp(step.V6 + step.V9, range);
output.V7 = Av1Transform1dMath.Clamp(step.V7 + step.V8, range);
output.V8 = Av1Transform1dMath.Clamp(step.V7 - step.V8, range);
output.V9 = Av1Transform1dMath.Clamp(step.V6 - step.V9, range);
output.V10 = Av1Transform1dMath.Clamp(step.V5 - step.V10, range);
output.V11 = Av1Transform1dMath.Clamp(step.V4 - step.V11, range);
output.V12 = Av1Transform1dMath.Clamp(step.V3 - step.V12, range);
output.V13 = Av1Transform1dMath.Clamp(step.V2 - step.V13, range);
output.V14 = Av1Transform1dMath.Clamp(step.V1 - step.V14, range);
output.V15 = Av1Transform1dMath.Clamp(step.V0 - step.V15, range);
}
}
}

1033
src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Dct32Operator.cs

File diff suppressed because it is too large

118
src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Dct4Operator.cs

@ -0,0 +1,118 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the four-point discrete cosine inverse transform operator.
/// </content>
internal static partial class Av1Inverse2dTransformer
{
/// <summary>
/// Defines the four-point AV1 inverse discrete cosine transform operator.
/// </summary>
/// <remarks>
/// Vector fields represent transform positions and vector lanes represent independent axes. The SIMD overloads apply
/// the same staged butterflies, fixed-point rounding, and range clamps as the scalar overload without mixing axes.
/// </remarks>
internal readonly struct Dct4Operator : IAv1InverseTransform1dOperator
{
/// <summary>
/// Applies the normative four-point AV1 inverse discrete cosine transform.
/// </summary>
/// <param name="input">The four frequency-domain coefficients.</param>
/// <param name="output">The four spatial-domain residual values.</param>
/// <param name="step">The four-element stage buffer owned by the containing two-dimensional transform.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange)
{
// AV1 stores coefficients in frequency order; this permutation restores the order expected by the staged DCT.
output[0] = input[0];
output[1] = input[2];
output[2] = input[1];
output[3] = input[3];
// Rotate the even and odd coefficient pairs using the same fixed-point basis as the forward transform.
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
step[0] = Av1Transform1dMath.HalfButterfly(cospi[32], output[0], cospi[32], output[1], cosBit);
step[1] = Av1Transform1dMath.HalfButterfly(cospi[32], output[0], -cospi[32], output[1], cosBit);
step[2] = Av1Transform1dMath.HalfButterfly(cospi[48], output[2], -cospi[16], output[3], cosBit);
step[3] = Av1Transform1dMath.HalfButterfly(cospi[16], output[2], cospi[48], output[3], cosBit);
// The terminal butterflies reconstruct spatial order and clamp every result to the normative stage range.
byte range = stageRange[3];
output[0] = Av1Transform1dMath.Clamp(step[0] + step[3], range);
output[1] = Av1Transform1dMath.Clamp(step[1] + step[2], range);
output[2] = Av1Transform1dMath.Clamp(step[1] - step[2], range);
output[3] = Av1Transform1dMath.Clamp(step[0] - step[3], range);
}
/// <inheritdoc/>
public static void Transform(
ref Av1TransformVector<Vector256<int>> input,
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
// AV1 stores coefficients in frequency order; this permutation restores the order expected by the staged DCT.
output.V0 = input.V0;
output.V1 = input.V2;
output.V2 = input.V1;
output.V3 = input.V3;
// Rotate the even and odd coefficient pairs using the same fixed-point basis as the forward transform.
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
step.V0 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, cospi[32], output.V1, cosBit);
step.V1 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, -cospi[32], output.V1, cosBit);
step.V2 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V2, -cospi[16], output.V3, cosBit);
step.V3 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V2, cospi[48], output.V3, cosBit);
// The terminal butterflies reconstruct spatial order and clamp every result to the normative stage range.
byte range = stageRange[3];
output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V3, range);
output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V2, range);
output.V2 = Av1Transform1dMath.Clamp(step.V1 - step.V2, range);
output.V3 = Av1Transform1dMath.Clamp(step.V0 - step.V3, range);
}
/// <summary>
/// Applies the transform to four independent axes in parallel.
/// </summary>
/// <param name="input">The source values for the parallel transform axes.</param>
/// <param name="output">The destination values for the parallel transform axes.</param>
/// <param name="step">The fixed stage storage for the parallel transform axes.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(
ref Av1TransformVector<Vector128<int>> input,
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
// AV1 stores coefficients in frequency order; this permutation restores the order expected by the staged DCT.
output.V0 = input.V0;
output.V1 = input.V2;
output.V2 = input.V1;
output.V3 = input.V3;
// Rotate the even and odd coefficient pairs using the same fixed-point basis as the forward transform.
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
step.V0 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, cospi[32], output.V1, cosBit);
step.V1 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, -cospi[32], output.V1, cosBit);
step.V2 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V2, -cospi[16], output.V3, cosBit);
step.V3 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V2, cospi[48], output.V3, cosBit);
// The terminal butterflies reconstruct spatial order and clamp every result to the normative stage range.
byte range = stageRange[3];
output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V3, range);
output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V2, range);
output.V2 = Av1Transform1dMath.Clamp(step.V1 - step.V2, range);
output.V3 = Av1Transform1dMath.Clamp(step.V0 - step.V3, range);
}
}
}

2278
src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Dct64Operator.cs

File diff suppressed because it is too large

238
src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Dct8Operator.cs

@ -0,0 +1,238 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the eight-point discrete cosine inverse transform operator.
/// </content>
internal static partial class Av1Inverse2dTransformer
{
/// <summary>
/// Defines the eight-point AV1 inverse discrete cosine transform operator.
/// </summary>
/// <remarks>
/// Vector fields represent transform positions and vector lanes represent independent axes. The SIMD overloads apply
/// the same staged butterflies, fixed-point rounding, and range clamps as the scalar overload without mixing axes.
/// </remarks>
internal readonly struct Dct8Operator : IAv1InverseTransform1dOperator
{
/// <summary>
/// Applies the normative eight-point AV1 inverse discrete cosine transform.
/// </summary>
/// <param name="input">The eight frequency-domain coefficients.</param>
/// <param name="output">The eight spatial-domain residual values.</param>
/// <param name="step">The eight-element stage buffer owned by the containing two-dimensional transform.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0;
// Stage 1 permutes frequency-ordered coefficients into the recursive DCT factorization order.
stage++;
output[0] = input[0];
output[1] = input[4];
output[2] = input[2];
output[3] = input[6];
output[4] = input[1];
output[5] = input[5];
output[6] = input[3];
output[7] = input[7];
// Stage 2 rotates the odd-frequency coefficient pairs by their pi/16 angles.
stage++;
step[0] = output[0];
step[1] = output[1];
step[2] = output[2];
step[3] = output[3];
step[4] = Av1Transform1dMath.HalfButterfly(cospi[56], output[4], -cospi[8], output[7], cosBit);
step[5] = Av1Transform1dMath.HalfButterfly(cospi[24], output[5], -cospi[40], output[6], cosBit);
step[6] = Av1Transform1dMath.HalfButterfly(cospi[40], output[5], cospi[24], output[6], cosBit);
step[7] = Av1Transform1dMath.HalfButterfly(cospi[8], output[4], cospi[56], output[7], cosBit);
// Stage 3 reconstructs the even four-point DCT and combines adjacent odd terms.
stage++;
byte range = stageRange[stage];
output[0] = Av1Transform1dMath.HalfButterfly(cospi[32], step[0], cospi[32], step[1], cosBit);
output[1] = Av1Transform1dMath.HalfButterfly(cospi[32], step[0], -cospi[32], step[1], cosBit);
output[2] = Av1Transform1dMath.HalfButterfly(cospi[48], step[2], -cospi[16], step[3], cosBit);
output[3] = Av1Transform1dMath.HalfButterfly(cospi[16], step[2], cospi[48], step[3], cosBit);
output[4] = Av1Transform1dMath.Clamp(step[4] + step[5], range);
output[5] = Av1Transform1dMath.Clamp(step[4] - step[5], range);
output[6] = Av1Transform1dMath.Clamp(step[7] - step[6], range);
output[7] = Av1Transform1dMath.Clamp(step[6] + step[7], range);
// Stage 4 completes the even butterflies and applies the remaining pi/4 odd rotation.
stage++;
step[0] = Av1Transform1dMath.Clamp(output[0] + output[3], range);
step[1] = Av1Transform1dMath.Clamp(output[1] + output[2], range);
step[2] = Av1Transform1dMath.Clamp(output[1] - output[2], range);
step[3] = Av1Transform1dMath.Clamp(output[0] - output[3], range);
step[4] = output[4];
step[5] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[5], cospi[32], output[6], cosBit);
step[6] = Av1Transform1dMath.HalfButterfly(cospi[32], output[5], cospi[32], output[6], cosBit);
step[7] = output[7];
// Stage 5 merges the even and odd halves into spatial order and clamps every result.
stage++;
range = stageRange[stage];
output[0] = Av1Transform1dMath.Clamp(step[0] + step[7], range);
output[1] = Av1Transform1dMath.Clamp(step[1] + step[6], range);
output[2] = Av1Transform1dMath.Clamp(step[2] + step[5], range);
output[3] = Av1Transform1dMath.Clamp(step[3] + step[4], range);
output[4] = Av1Transform1dMath.Clamp(step[3] - step[4], range);
output[5] = Av1Transform1dMath.Clamp(step[2] - step[5], range);
output[6] = Av1Transform1dMath.Clamp(step[1] - step[6], range);
output[7] = Av1Transform1dMath.Clamp(step[0] - step[7], range);
}
/// <inheritdoc/>
public static void Transform(
ref Av1TransformVector<Vector256<int>> input,
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0;
// Stage 1 permutes frequency-ordered coefficients into the recursive DCT factorization order.
stage++;
output.V0 = input.V0;
output.V1 = input.V4;
output.V2 = input.V2;
output.V3 = input.V6;
output.V4 = input.V1;
output.V5 = input.V5;
output.V6 = input.V3;
output.V7 = input.V7;
// Stage 2 rotates the odd-frequency coefficient pairs by their pi/16 angles.
stage++;
step.V0 = output.V0;
step.V1 = output.V1;
step.V2 = output.V2;
step.V3 = output.V3;
step.V4 = Av1Transform1dMath.HalfButterfly(cospi[56], output.V4, -cospi[8], output.V7, cosBit);
step.V5 = Av1Transform1dMath.HalfButterfly(cospi[24], output.V5, -cospi[40], output.V6, cosBit);
step.V6 = Av1Transform1dMath.HalfButterfly(cospi[40], output.V5, cospi[24], output.V6, cosBit);
step.V7 = Av1Transform1dMath.HalfButterfly(cospi[8], output.V4, cospi[56], output.V7, cosBit);
// Stage 3 reconstructs the even four-point DCT and combines adjacent odd terms.
stage++;
byte range = stageRange[stage];
output.V0 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V0, cospi[32], step.V1, cosBit);
output.V1 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V0, -cospi[32], step.V1, cosBit);
output.V2 = Av1Transform1dMath.HalfButterfly(cospi[48], step.V2, -cospi[16], step.V3, cosBit);
output.V3 = Av1Transform1dMath.HalfButterfly(cospi[16], step.V2, cospi[48], step.V3, cosBit);
output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V5, range);
output.V5 = Av1Transform1dMath.Clamp(step.V4 - step.V5, range);
output.V6 = Av1Transform1dMath.Clamp(step.V7 - step.V6, range);
output.V7 = Av1Transform1dMath.Clamp(step.V6 + step.V7, range);
// Stage 4 completes the even butterflies and applies the remaining pi/4 odd rotation.
stage++;
step.V0 = Av1Transform1dMath.Clamp(output.V0 + output.V3, range);
step.V1 = Av1Transform1dMath.Clamp(output.V1 + output.V2, range);
step.V2 = Av1Transform1dMath.Clamp(output.V1 - output.V2, range);
step.V3 = Av1Transform1dMath.Clamp(output.V0 - output.V3, range);
step.V4 = output.V4;
step.V5 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V5, cospi[32], output.V6, cosBit);
step.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V5, cospi[32], output.V6, cosBit);
step.V7 = output.V7;
// Stage 5 merges the even and odd halves into spatial order and clamps every result.
stage++;
range = stageRange[stage];
output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V7, range);
output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V6, range);
output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V5, range);
output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V4, range);
output.V4 = Av1Transform1dMath.Clamp(step.V3 - step.V4, range);
output.V5 = Av1Transform1dMath.Clamp(step.V2 - step.V5, range);
output.V6 = Av1Transform1dMath.Clamp(step.V1 - step.V6, range);
output.V7 = Av1Transform1dMath.Clamp(step.V0 - step.V7, range);
}
/// <summary>
/// Applies the transform to four independent axes in parallel.
/// </summary>
/// <param name="input">The source values for the parallel transform axes.</param>
/// <param name="output">The destination values for the parallel transform axes.</param>
/// <param name="step">The fixed stage storage for the parallel transform axes.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static void Transform(
ref Av1TransformVector<Vector128<int>> input,
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
ReadOnlySpan<int> cospi = Av1SinusConstants.CosinusPi(cosBit);
int stage = 0;
// Stage 1 permutes frequency-ordered coefficients into the recursive DCT factorization order.
stage++;
output.V0 = input.V0;
output.V1 = input.V4;
output.V2 = input.V2;
output.V3 = input.V6;
output.V4 = input.V1;
output.V5 = input.V5;
output.V6 = input.V3;
output.V7 = input.V7;
// Stage 2 rotates the odd-frequency coefficient pairs by their pi/16 angles.
stage++;
step.V0 = output.V0;
step.V1 = output.V1;
step.V2 = output.V2;
step.V3 = output.V3;
step.V4 = Av1Transform1dMath.HalfButterfly(cospi[56], output.V4, -cospi[8], output.V7, cosBit);
step.V5 = Av1Transform1dMath.HalfButterfly(cospi[24], output.V5, -cospi[40], output.V6, cosBit);
step.V6 = Av1Transform1dMath.HalfButterfly(cospi[40], output.V5, cospi[24], output.V6, cosBit);
step.V7 = Av1Transform1dMath.HalfButterfly(cospi[8], output.V4, cospi[56], output.V7, cosBit);
// Stage 3 reconstructs the even four-point DCT and combines adjacent odd terms.
stage++;
byte range = stageRange[stage];
output.V0 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V0, cospi[32], step.V1, cosBit);
output.V1 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V0, -cospi[32], step.V1, cosBit);
output.V2 = Av1Transform1dMath.HalfButterfly(cospi[48], step.V2, -cospi[16], step.V3, cosBit);
output.V3 = Av1Transform1dMath.HalfButterfly(cospi[16], step.V2, cospi[48], step.V3, cosBit);
output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V5, range);
output.V5 = Av1Transform1dMath.Clamp(step.V4 - step.V5, range);
output.V6 = Av1Transform1dMath.Clamp(step.V7 - step.V6, range);
output.V7 = Av1Transform1dMath.Clamp(step.V6 + step.V7, range);
// Stage 4 completes the even butterflies and applies the remaining pi/4 odd rotation.
stage++;
step.V0 = Av1Transform1dMath.Clamp(output.V0 + output.V3, range);
step.V1 = Av1Transform1dMath.Clamp(output.V1 + output.V2, range);
step.V2 = Av1Transform1dMath.Clamp(output.V1 - output.V2, range);
step.V3 = Av1Transform1dMath.Clamp(output.V0 - output.V3, range);
step.V4 = output.V4;
step.V5 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V5, cospi[32], output.V6, cosBit);
step.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V5, cospi[32], output.V6, cosBit);
step.V7 = output.V7;
// Stage 5 merges the even and odd halves into spatial order and clamps every result.
stage++;
range = stageRange[stage];
output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V7, range);
output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V6, range);
output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V5, range);
output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V4, range);
output.V4 = Av1Transform1dMath.Clamp(step.V3 - step.V4, range);
output.V5 = Av1Transform1dMath.Clamp(step.V2 - step.V5, range);
output.V6 = Av1Transform1dMath.Clamp(step.V1 - step.V6, range);
output.V7 = Av1Transform1dMath.Clamp(step.V0 - step.V7, range);
}
}
}

71
src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Identity16Operator.cs

@ -0,0 +1,71 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the sixteen-point identity inverse transform operator.
/// </content>
internal static partial class Av1Inverse2dTransformer
{
/// <summary>
/// Defines the sixteen-point AV1 inverse identity transform operator.
/// </summary>
/// <remarks>
/// Vector fields represent transform positions and vector lanes represent independent axes. Scaling is lane-local,
/// so the SIMD overloads preserve the scalar fixed-point multiplier and rounding for every axis.
/// </remarks>
internal readonly struct Identity16Operator : IAv1InverseTransform1dOperator
{
/// <summary>
/// Applies the normative sixteen-point AV1 inverse identity transform.
/// </summary>
/// <param name="input">The sixteen frequency-domain coefficients.</param>
/// <param name="output">The sixteen scaled spatial-domain values.</param>
/// <param name="step">Unused stage storage supplied by the common transform-kernel contract.</param>
/// <param name="cosBit">Unused cosine precision supplied by the common transform-kernel contract.</param>
/// <param name="stageRange">The signed-bit range assigned to the transform output.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange)
{
_ = step;
_ = cosBit;
_ = stageRange;
// The AV1 identity transform preserves coefficient order while applying the twice the square-root-of-two fixed-point scale required for 2-D normalization.
for (int i = 0; i < 16; i++)
{
output[i] = Av1Math.RoundShift((long)input[i] * (2 * Av1Transform1dMath.NewSqrt2), Av1Transform1dMath.NewSqrt2Bits);
}
}
/// <inheritdoc/>
public static void Transform(
ref Av1TransformVector<Vector128<int>> input,
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
Av1IdentityTransform1d.Transform(ref input, ref output, 16, 2 * Av1Transform1dMath.NewSqrt2, Av1Transform1dMath.NewSqrt2Bits);
_ = step;
_ = cosBit;
_ = stageRange;
}
/// <inheritdoc/>
public static void Transform(
ref Av1TransformVector<Vector256<int>> input,
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
Av1IdentityTransform1d.Transform(ref input, ref output, 16, 2 * Av1Transform1dMath.NewSqrt2, Av1Transform1dMath.NewSqrt2Bits);
_ = step;
_ = cosBit;
_ = stageRange;
}
}
}

71
src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Identity32Operator.cs

@ -0,0 +1,71 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the thirty-two-point identity inverse transform operator.
/// </content>
internal static partial class Av1Inverse2dTransformer
{
/// <summary>
/// Defines the thirty-two-point AV1 inverse identity transform operator.
/// </summary>
/// <remarks>
/// Vector fields represent transform positions and vector lanes represent independent axes. Scaling is lane-local,
/// so the SIMD overloads preserve the scalar fixed-point multiplier and rounding for every axis.
/// </remarks>
internal readonly struct Identity32Operator : IAv1InverseTransform1dOperator
{
/// <summary>
/// Applies the normative thirty-two-point AV1 inverse identity transform.
/// </summary>
/// <param name="input">The thirty-two frequency-domain coefficients.</param>
/// <param name="output">The thirty-two scaled spatial-domain values.</param>
/// <param name="step">Unused stage storage supplied by the common transform-kernel contract.</param>
/// <param name="cosBit">Unused cosine precision supplied by the common transform-kernel contract.</param>
/// <param name="stageRange">The signed-bit range assigned to the transform output.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange)
{
_ = step;
_ = cosBit;
_ = stageRange;
// The AV1 identity transform preserves coefficient order while applying the exact factor-of-four scale required for 2-D normalization.
for (int i = 0; i < 32; i++)
{
output[i] = input[i] * 4;
}
}
/// <inheritdoc/>
public static void Transform(
ref Av1TransformVector<Vector128<int>> input,
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
Av1IdentityTransform1d.Transform(ref input, ref output, 32, 4, 0);
_ = step;
_ = cosBit;
_ = stageRange;
}
/// <inheritdoc/>
public static void Transform(
ref Av1TransformVector<Vector256<int>> input,
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
Av1IdentityTransform1d.Transform(ref input, ref output, 32, 4, 0);
_ = step;
_ = cosBit;
_ = stageRange;
}
}
}

71
src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Identity4Operator.cs

@ -0,0 +1,71 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the four-point identity inverse transform operator.
/// </content>
internal static partial class Av1Inverse2dTransformer
{
/// <summary>
/// Defines the four-point AV1 inverse identity transform operator.
/// </summary>
/// <remarks>
/// Vector fields represent transform positions and vector lanes represent independent axes. Scaling is lane-local,
/// so the SIMD overloads preserve the scalar fixed-point multiplier and rounding for every axis.
/// </remarks>
internal readonly struct Identity4Operator : IAv1InverseTransform1dOperator
{
/// <summary>
/// Applies the normative four-point AV1 inverse identity transform.
/// </summary>
/// <param name="input">The four frequency-domain coefficients.</param>
/// <param name="output">The four scaled spatial-domain values.</param>
/// <param name="step">Unused stage storage supplied by the common transform-kernel contract.</param>
/// <param name="cosBit">Unused cosine precision supplied by the common transform-kernel contract.</param>
/// <param name="stageRange">The signed-bit range assigned to the transform output.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange)
{
_ = step;
_ = cosBit;
_ = stageRange;
// The AV1 identity transform preserves coefficient order while applying the square-root-of-two fixed-point scale required for 2-D normalization.
for (int i = 0; i < 4; i++)
{
output[i] = Av1Math.RoundShift((long)input[i] * Av1Transform1dMath.NewSqrt2, Av1Transform1dMath.NewSqrt2Bits);
}
}
/// <inheritdoc/>
public static void Transform(
ref Av1TransformVector<Vector128<int>> input,
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
Av1IdentityTransform1d.Transform(ref input, ref output, 4, Av1Transform1dMath.NewSqrt2, Av1Transform1dMath.NewSqrt2Bits);
_ = step;
_ = cosBit;
_ = stageRange;
}
/// <inheritdoc/>
public static void Transform(
ref Av1TransformVector<Vector256<int>> input,
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
Av1IdentityTransform1d.Transform(ref input, ref output, 4, Av1Transform1dMath.NewSqrt2, Av1Transform1dMath.NewSqrt2Bits);
_ = step;
_ = cosBit;
_ = stageRange;
}
}
}

71
src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Identity8Operator.cs

@ -0,0 +1,71 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the eight-point identity inverse transform operator.
/// </content>
internal static partial class Av1Inverse2dTransformer
{
/// <summary>
/// Defines the eight-point AV1 inverse identity transform operator.
/// </summary>
/// <remarks>
/// Vector fields represent transform positions and vector lanes represent independent axes. Scaling is lane-local,
/// so the SIMD overloads preserve the scalar fixed-point multiplier and rounding for every axis.
/// </remarks>
internal readonly struct Identity8Operator : IAv1InverseTransform1dOperator
{
/// <summary>
/// Applies the normative eight-point AV1 inverse identity transform.
/// </summary>
/// <param name="input">The eight frequency-domain coefficients.</param>
/// <param name="output">The eight scaled spatial-domain values.</param>
/// <param name="step">Unused stage storage supplied by the common transform-kernel contract.</param>
/// <param name="cosBit">Unused cosine precision supplied by the common transform-kernel contract.</param>
/// <param name="stageRange">The signed-bit range assigned to the transform output.</param>
public static void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange)
{
_ = step;
_ = cosBit;
_ = stageRange;
// The AV1 identity transform preserves coefficient order while applying the exact factor-of-two scale required for 2-D normalization.
for (int i = 0; i < 8; i++)
{
output[i] = input[i] * 2;
}
}
/// <inheritdoc/>
public static void Transform(
ref Av1TransformVector<Vector128<int>> input,
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
Av1IdentityTransform1d.Transform(ref input, ref output, 8, 2, 0);
_ = step;
_ = cosBit;
_ = stageRange;
}
/// <inheritdoc/>
public static void Transform(
ref Av1TransformVector<Vector256<int>> input,
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange)
{
Av1IdentityTransform1d.Transform(ref input, ref output, 8, 2, 0);
_ = step;
_ = cosBit;
_ = stageRange;
}
}
}

166
src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.cs → src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.Operator.cs

@ -4,7 +4,6 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Inverse;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
@ -17,8 +16,101 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// cross-lane permutations. Reconstruction adds the final residuals to their matching prediction lanes before
/// narrowing to the decoded sample depth.
/// </remarks>
internal static class Av1Inverse2dTransformer
internal static partial class Av1Inverse2dTransformer
{
/// <summary>
/// Defines the scalar and SIMD arithmetic for one AV1 one-dimensional inverse transform.
/// </summary>
/// <remarks>
/// Each overload performs the same staged fixed-point inverse transform. In the SIMD overloads, each vector field identifies
/// one coefficient position and each lane identifies an independent row or column. Butterfly arithmetic is therefore
/// lane-local: vectorization changes only how many axes advance together, not coefficient order, rounding, or stage
/// clamping. The two-dimensional traversal selects the concrete operator and lane width once per block, allowing the
/// JIT to specialize every static interface call outside the stage network.
/// </remarks>
internal interface IAv1InverseTransform1dOperator
{
/// <summary>
/// Transforms one axis when hardware vectorization is unavailable.
/// </summary>
/// <param name="input">The source values for the transform axis.</param>
/// <param name="output">The destination values for the transform axis.</param>
/// <param name="step">The fixed stage storage for the transform axis.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static abstract void Transform(ReadOnlySpan<int> input, Span<int> output, Span<int> step, int cosBit, Av1TransformStageRange stageRange);
/// <summary>
/// Transforms four independent axes in parallel.
/// </summary>
/// <param name="input">The source values for four transform axes.</param>
/// <param name="output">The destination values for four transform axes.</param>
/// <param name="step">The fixed stage storage for four transform axes.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static abstract void Transform(
ref Av1TransformVector<Vector128<int>> input,
ref Av1TransformVector<Vector128<int>> output,
ref Av1TransformVector<Vector128<int>> step,
int cosBit,
Av1TransformStageRange stageRange);
/// <summary>
/// Transforms eight independent axes in parallel.
/// </summary>
/// <param name="input">The source values for eight transform axes.</param>
/// <param name="output">The destination values for eight transform axes.</param>
/// <param name="step">The fixed stage storage for eight transform axes.</param>
/// <param name="cosBit">The fixed-point precision of the cosine constants.</param>
/// <param name="stageRange">The signed-bit range assigned to each transform stage.</param>
public static abstract void Transform(
ref Av1TransformVector<Vector256<int>> input,
ref Av1TransformVector<Vector256<int>> output,
ref Av1TransformVector<Vector256<int>> step,
int cosBit,
Av1TransformStageRange stageRange);
}
/// <summary>
/// Defines how inverse-transform residuals are added to a decoded sample representation.
/// </summary>
/// <remarks>
/// Residual lanes correspond to consecutive reconstructed samples. Implementations must widen packed predictions,
/// add and clip in signed 32-bit lanes, then store exactly four or eight results so callers do not require writable
/// padding beyond the transform block. The closed sample type allows byte and high-bit-depth storage to specialize.
/// </remarks>
/// <typeparam name="TSample">The decoded sample storage type.</typeparam>
internal interface IAv1InverseTransformOutputOperator<TSample>
where TSample : unmanaged
{
/// <summary>
/// Adds one residual to a predicted sample and clips the result to the coded bit depth.
/// </summary>
/// <param name="prediction">The predicted sample.</param>
/// <param name="residual">The inverse-transform residual.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
/// <returns>The reconstructed sample.</returns>
public static abstract TSample Add(TSample prediction, int residual, int bitDepth);
/// <summary>
/// Adds four residuals to four predicted samples and stores the clipped results.
/// </summary>
/// <param name="prediction">The first predicted sample.</param>
/// <param name="destination">The first destination sample.</param>
/// <param name="residual">The four inverse-transform residuals.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
public static abstract void Add(ref TSample prediction, ref TSample destination, Vector128<int> residual, int bitDepth);
/// <summary>
/// Adds eight residuals to eight predicted samples and stores the clipped results.
/// </summary>
/// <param name="prediction">The first predicted sample.</param>
/// <param name="destination">The first destination sample.</param>
/// <param name="residual">The eight inverse-transform residuals.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
public static abstract void Add(ref TSample prediction, ref TSample destination, Vector256<int> residual, int bitDepth);
}
/// <summary>
/// Applies an inverse transform and adds its residual to high-bit-depth predicted samples.
/// </summary>
@ -39,7 +131,7 @@ internal static class Av1Inverse2dTransformer
ref Av1Transform2dFlipConfiguration config,
Span<int> workspace,
int bitDepth)
=> Transform2dAdd<short, Av1InverseTransformOutputOperator<short>>(
=> Transform2dAdd<short, OutputOperator<short>>(
input,
outputForRead,
strideForRead,
@ -67,7 +159,7 @@ internal static class Av1Inverse2dTransformer
int strideForWrite,
ref Av1Transform2dFlipConfiguration config,
Span<int> workspace)
=> Transform2dAdd<byte, Av1InverseTransformOutputOperator<byte>>(
=> Transform2dAdd<byte, OutputOperator<byte>>(
input,
outputForRead,
strideForRead,
@ -96,62 +188,62 @@ internal static class Av1Inverse2dTransformer
switch (config.TransformFunctionTypeColumn)
{
case Av1TransformFunctionType.Dct4:
DispatchRow<TSample, TOutputOperator, Av1Dct4Inverse1dOperator>(
DispatchRow<TSample, TOutputOperator, Dct4Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Dct8:
DispatchRow<TSample, TOutputOperator, Av1Dct8Inverse1dOperator>(
DispatchRow<TSample, TOutputOperator, Dct8Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Dct16:
DispatchRow<TSample, TOutputOperator, Av1Dct16Inverse1dOperator>(
DispatchRow<TSample, TOutputOperator, Dct16Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Dct32:
DispatchRow<TSample, TOutputOperator, Av1Dct32Inverse1dOperator>(
DispatchRow<TSample, TOutputOperator, Dct32Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Dct64:
DispatchRow<TSample, TOutputOperator, Av1Dct64Inverse1dOperator>(
DispatchRow<TSample, TOutputOperator, Dct64Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Adst4:
DispatchRow<TSample, TOutputOperator, Av1Adst4Inverse1dOperator>(
DispatchRow<TSample, TOutputOperator, Adst4Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Adst8:
DispatchRow<TSample, TOutputOperator, Av1Adst8Inverse1dOperator>(
DispatchRow<TSample, TOutputOperator, Adst8Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Adst16:
DispatchRow<TSample, TOutputOperator, Av1Adst16Inverse1dOperator>(
DispatchRow<TSample, TOutputOperator, Adst16Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Identity4:
DispatchRow<TSample, TOutputOperator, Av1Identity4Inverse1dOperator>(
DispatchRow<TSample, TOutputOperator, Identity4Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Identity8:
DispatchRow<TSample, TOutputOperator, Av1Identity8Inverse1dOperator>(
DispatchRow<TSample, TOutputOperator, Identity8Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Identity16:
DispatchRow<TSample, TOutputOperator, Av1Identity16Inverse1dOperator>(
DispatchRow<TSample, TOutputOperator, Identity16Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Identity32:
DispatchRow<TSample, TOutputOperator, Av1Identity32Inverse1dOperator>(
DispatchRow<TSample, TOutputOperator, Identity32Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
@ -174,67 +266,67 @@ internal static class Av1Inverse2dTransformer
int bitDepth)
where TSample : unmanaged
where TOutputOperator : struct, IAv1InverseTransformOutputOperator<TSample>
where TColumnOperator : struct, IAv1Transform1dOperator
where TColumnOperator : struct, IAv1InverseTransform1dOperator
{
switch (config.TransformFunctionTypeRow)
{
case Av1TransformFunctionType.Dct4:
Transform2d<TSample, TOutputOperator, TColumnOperator, Av1Dct4Inverse1dOperator>(
Transform2d<TSample, TOutputOperator, TColumnOperator, Dct4Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Dct8:
Transform2d<TSample, TOutputOperator, TColumnOperator, Av1Dct8Inverse1dOperator>(
Transform2d<TSample, TOutputOperator, TColumnOperator, Dct8Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Dct16:
Transform2d<TSample, TOutputOperator, TColumnOperator, Av1Dct16Inverse1dOperator>(
Transform2d<TSample, TOutputOperator, TColumnOperator, Dct16Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Dct32:
Transform2d<TSample, TOutputOperator, TColumnOperator, Av1Dct32Inverse1dOperator>(
Transform2d<TSample, TOutputOperator, TColumnOperator, Dct32Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Dct64:
Transform2d<TSample, TOutputOperator, TColumnOperator, Av1Dct64Inverse1dOperator>(
Transform2d<TSample, TOutputOperator, TColumnOperator, Dct64Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Adst4:
Transform2d<TSample, TOutputOperator, TColumnOperator, Av1Adst4Inverse1dOperator>(
Transform2d<TSample, TOutputOperator, TColumnOperator, Adst4Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Adst8:
Transform2d<TSample, TOutputOperator, TColumnOperator, Av1Adst8Inverse1dOperator>(
Transform2d<TSample, TOutputOperator, TColumnOperator, Adst8Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Adst16:
Transform2d<TSample, TOutputOperator, TColumnOperator, Av1Adst16Inverse1dOperator>(
Transform2d<TSample, TOutputOperator, TColumnOperator, Adst16Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Identity4:
Transform2d<TSample, TOutputOperator, TColumnOperator, Av1Identity4Inverse1dOperator>(
Transform2d<TSample, TOutputOperator, TColumnOperator, Identity4Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Identity8:
Transform2d<TSample, TOutputOperator, TColumnOperator, Av1Identity8Inverse1dOperator>(
Transform2d<TSample, TOutputOperator, TColumnOperator, Identity8Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Identity16:
Transform2d<TSample, TOutputOperator, TColumnOperator, Av1Identity16Inverse1dOperator>(
Transform2d<TSample, TOutputOperator, TColumnOperator, Identity16Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
case Av1TransformFunctionType.Identity32:
Transform2d<TSample, TOutputOperator, TColumnOperator, Av1Identity32Inverse1dOperator>(
Transform2d<TSample, TOutputOperator, TColumnOperator, Identity32Operator>(
input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth);
break;
@ -257,8 +349,8 @@ internal static class Av1Inverse2dTransformer
int bitDepth)
where TSample : unmanaged
where TOutputOperator : struct, IAv1InverseTransformOutputOperator<TSample>
where TColumnOperator : struct, IAv1Transform1dOperator
where TRowOperator : struct, IAv1Transform1dOperator
where TColumnOperator : struct, IAv1InverseTransform1dOperator
where TRowOperator : struct, IAv1InverseTransform1dOperator
{
int width = config.TransformSize.GetWidth();
int height = config.TransformSize.GetHeight();
@ -309,8 +401,8 @@ internal static class Av1Inverse2dTransformer
int bitDepth)
where TSample : unmanaged
where TOutputOperator : struct, IAv1InverseTransformOutputOperator<TSample>
where TColumnOperator : struct, IAv1Transform1dOperator
where TRowOperator : struct, IAv1Transform1dOperator
where TColumnOperator : struct, IAv1InverseTransform1dOperator
where TRowOperator : struct, IAv1InverseTransform1dOperator
{
const int laneCount = 8;
const int vectorLength = Av1Constants.MaxTransformSize * laneCount;
@ -465,8 +557,8 @@ internal static class Av1Inverse2dTransformer
int bitDepth)
where TSample : unmanaged
where TOutputOperator : struct, IAv1InverseTransformOutputOperator<TSample>
where TColumnOperator : struct, IAv1Transform1dOperator
where TRowOperator : struct, IAv1Transform1dOperator
where TColumnOperator : struct, IAv1InverseTransform1dOperator
where TRowOperator : struct, IAv1InverseTransform1dOperator
{
const int laneCount = 4;
const int vectorLength = Av1Constants.MaxTransformSize * laneCount;
@ -597,8 +689,8 @@ internal static class Av1Inverse2dTransformer
int bitDepth)
where TSample : unmanaged
where TOutputOperator : struct, IAv1InverseTransformOutputOperator<TSample>
where TColumnOperator : struct, IAv1Transform1dOperator
where TRowOperator : struct, IAv1Transform1dOperator
where TColumnOperator : struct, IAv1InverseTransform1dOperator
where TRowOperator : struct, IAv1InverseTransform1dOperator
{
int width = config.TransformSize.GetWidth();
int height = config.TransformSize.GetHeight();

100
src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.OutputOperator.cs

@ -0,0 +1,100 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <content>
/// Provides the sample-output operator shared by inverse transform traversals.
/// </content>
internal static partial class Av1Inverse2dTransformer
{
/// <summary>
/// Reconstructs AV1 samples from predicted values and inverse-transform residuals.
/// </summary>
/// <remarks>
/// Each SIMD lane represents one consecutive reconstructed sample. Packed byte or 16-bit predictions are widened to
/// signed 32-bit lanes before residual addition, clipped to the coded sample range, and narrowed into exact-width
/// stores. The closed <typeparamref name="TSample"/> specialization removes storage-type branches from hot loops.
/// </remarks>
/// <typeparam name="TSample">The decoded sample storage type.</typeparam>
internal readonly struct OutputOperator<TSample> : IAv1InverseTransformOutputOperator<TSample>
where TSample : unmanaged
{
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TSample Add(TSample prediction, int residual, int bitDepth)
{
// TSample is fixed by the byte and short decoder entry points. The JIT removes this type test from each
// closed transform so storage selection does not introduce a branch in the reconstruction loop.
if (typeof(TSample) == typeof(byte))
{
byte value = (byte)Math.Clamp(Unsafe.As<TSample, byte>(ref prediction) + residual, byte.MinValue, byte.MaxValue);
return Unsafe.As<byte, TSample>(ref value);
}
short result = (short)Math.Clamp(Unsafe.As<TSample, short>(ref prediction) + residual, 0, (1 << bitDepth) - 1);
return Unsafe.As<short, TSample>(ref result);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add(ref TSample prediction, ref TSample destination, Vector128<int> residual, int bitDepth)
{
if (typeof(TSample) == typeof(byte))
{
// Read and write exactly four bytes. The unused upper lanes only participate in narrowing and never reach
// memory, which keeps reconstruction valid at a tightly packed row boundary.
ref byte source = ref Unsafe.As<TSample, byte>(ref prediction);
uint packed = Unsafe.ReadUnaligned<uint>(ref source);
Vector128<ushort> predicted16 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte());
Vector128<int> predicted32 = Vector128.WidenLower(predicted16).AsInt32();
Vector128<int> reconstructed = Vector128.Clamp(predicted32 + residual, Vector128<int>.Zero, Vector128.Create((int)byte.MaxValue));
Vector128<ushort> reconstructed16 = Vector128.Narrow(reconstructed.AsUInt32(), Vector128<uint>.Zero);
Vector128<byte> reconstructed8 = Vector128.Narrow(reconstructed16, Vector128<ushort>.Zero);
Unsafe.WriteUnaligned(ref Unsafe.As<TSample, byte>(ref destination), reconstructed8.AsUInt32().ToScalar());
return;
}
ref short highBitDepthSource = ref Unsafe.As<TSample, short>(ref prediction);
ulong highBitDepthPacked = Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<short, byte>(ref highBitDepthSource));
Vector128<int> highBitDepthPredicted = Vector128.WidenLower(Vector128.CreateScalarUnsafe(highBitDepthPacked).AsInt16());
Vector128<int> highBitDepthReconstructed =
Vector128.Clamp(highBitDepthPredicted + residual, Vector128<int>.Zero, Vector128.Create((1 << bitDepth) - 1));
Vector128<short> narrowed = Vector128.Narrow(highBitDepthReconstructed, Vector128<int>.Zero);
Unsafe.WriteUnaligned(ref Unsafe.As<TSample, byte>(ref destination), narrowed.AsUInt64().ToScalar());
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add(ref TSample prediction, ref TSample destination, Vector256<int> residual, int bitDepth)
{
if (typeof(TSample) == typeof(byte))
{
// Eight byte predictions widen through UInt16 into the eight Int32 residual lanes. The final 64-bit store
// covers only those reconstructed samples and does not require destination padding.
ref byte source = ref Unsafe.As<TSample, byte>(ref prediction);
ulong packed = Unsafe.ReadUnaligned<ulong>(ref source);
Vector128<ushort> predicted16 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte());
Vector256<int> predicted32 = Vector256.Create(Vector128.WidenLower(predicted16), Vector128.WidenUpper(predicted16)).AsInt32();
Vector256<int> reconstructed = Vector256.Clamp(predicted32 + residual, Vector256<int>.Zero, Vector256.Create((int)byte.MaxValue));
Vector128<ushort> reconstructed16 = Vector128.Narrow(reconstructed.GetLower().AsUInt32(), reconstructed.GetUpper().AsUInt32());
Vector128<byte> reconstructed8 = Vector128.Narrow(reconstructed16, Vector128<ushort>.Zero);
Unsafe.WriteUnaligned(ref Unsafe.As<TSample, byte>(ref destination), reconstructed8.AsUInt64().ToScalar());
return;
}
ref short highBitDepthSource = ref Unsafe.As<TSample, short>(ref prediction);
Vector256<int> highBitDepthPredicted = Vector256_.Widen(Vector128.LoadUnsafe(ref highBitDepthSource));
Vector256<int> highBitDepthReconstructed =
Vector256.Clamp(highBitDepthPredicted + residual, Vector256<int>.Zero, Vector256.Create((1 << bitDepth) - 1));
Vector128<short> narrowed = Vector128.Narrow(highBitDepthReconstructed.GetLower(), highBitDepthReconstructed.GetUpper());
narrowed.StoreUnsafe(ref Unsafe.As<TSample, short>(ref destination));
}
}
}

94
src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformOutputOperator.cs

@ -1,94 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
/// <summary>
/// Reconstructs AV1 samples from predicted values and inverse-transform residuals.
/// </summary>
/// <remarks>
/// Each SIMD lane represents one consecutive reconstructed sample. Packed byte or 16-bit predictions are widened to
/// signed 32-bit lanes before residual addition, clipped to the coded sample range, and narrowed into exact-width
/// stores. The closed <typeparamref name="TSample"/> specialization removes storage-type branches from hot loops.
/// </remarks>
/// <typeparam name="TSample">The decoded sample storage type.</typeparam>
internal readonly struct Av1InverseTransformOutputOperator<TSample> : IAv1InverseTransformOutputOperator<TSample>
where TSample : unmanaged
{
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TSample Add(TSample prediction, int residual, int bitDepth)
{
// TSample is fixed by the byte and short decoder entry points. The JIT removes this type test from each
// closed transform so storage selection does not introduce a branch in the reconstruction loop.
if (typeof(TSample) == typeof(byte))
{
byte value = (byte)Math.Clamp(Unsafe.As<TSample, byte>(ref prediction) + residual, byte.MinValue, byte.MaxValue);
return Unsafe.As<byte, TSample>(ref value);
}
short result = (short)Math.Clamp(Unsafe.As<TSample, short>(ref prediction) + residual, 0, (1 << bitDepth) - 1);
return Unsafe.As<short, TSample>(ref result);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add(ref TSample prediction, ref TSample destination, Vector128<int> residual, int bitDepth)
{
if (typeof(TSample) == typeof(byte))
{
// Read and write exactly four bytes. The unused upper lanes only participate in narrowing and never reach
// memory, which keeps reconstruction valid at a tightly packed row boundary.
ref byte source = ref Unsafe.As<TSample, byte>(ref prediction);
uint packed = Unsafe.ReadUnaligned<uint>(ref source);
Vector128<ushort> predicted16 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte());
Vector128<int> predicted32 = Vector128.WidenLower(predicted16).AsInt32();
Vector128<int> reconstructed = Vector128.Clamp(predicted32 + residual, Vector128<int>.Zero, Vector128.Create((int)byte.MaxValue));
Vector128<ushort> reconstructed16 = Vector128.Narrow(reconstructed.AsUInt32(), Vector128<uint>.Zero);
Vector128<byte> reconstructed8 = Vector128.Narrow(reconstructed16, Vector128<ushort>.Zero);
Unsafe.WriteUnaligned(ref Unsafe.As<TSample, byte>(ref destination), reconstructed8.AsUInt32().ToScalar());
return;
}
ref short highBitDepthSource = ref Unsafe.As<TSample, short>(ref prediction);
ulong highBitDepthPacked = Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<short, byte>(ref highBitDepthSource));
Vector128<int> highBitDepthPredicted = Vector128.WidenLower(Vector128.CreateScalarUnsafe(highBitDepthPacked).AsInt16());
Vector128<int> highBitDepthReconstructed =
Vector128.Clamp(highBitDepthPredicted + residual, Vector128<int>.Zero, Vector128.Create((1 << bitDepth) - 1));
Vector128<short> narrowed = Vector128.Narrow(highBitDepthReconstructed, Vector128<int>.Zero);
Unsafe.WriteUnaligned(ref Unsafe.As<TSample, byte>(ref destination), narrowed.AsUInt64().ToScalar());
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add(ref TSample prediction, ref TSample destination, Vector256<int> residual, int bitDepth)
{
if (typeof(TSample) == typeof(byte))
{
// Eight byte predictions widen through UInt16 into the eight Int32 residual lanes. The final 64-bit store
// covers only those reconstructed samples and does not require destination padding.
ref byte source = ref Unsafe.As<TSample, byte>(ref prediction);
ulong packed = Unsafe.ReadUnaligned<ulong>(ref source);
Vector128<ushort> predicted16 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte());
Vector256<int> predicted32 = Vector256.Create(Vector128.WidenLower(predicted16), Vector128.WidenUpper(predicted16)).AsInt32();
Vector256<int> reconstructed = Vector256.Clamp(predicted32 + residual, Vector256<int>.Zero, Vector256.Create((int)byte.MaxValue));
Vector128<ushort> reconstructed16 = Vector128.Narrow(reconstructed.GetLower().AsUInt32(), reconstructed.GetUpper().AsUInt32());
Vector128<byte> reconstructed8 = Vector128.Narrow(reconstructed16, Vector128<ushort>.Zero);
Unsafe.WriteUnaligned(ref Unsafe.As<TSample, byte>(ref destination), reconstructed8.AsUInt64().ToScalar());
return;
}
ref short highBitDepthSource = ref Unsafe.As<TSample, short>(ref prediction);
Vector256<int> highBitDepthPredicted = Vector256_.Widen(Vector128.LoadUnsafe(ref highBitDepthSource));
Vector256<int> highBitDepthReconstructed =
Vector256.Clamp(highBitDepthPredicted + residual, Vector256<int>.Zero, Vector256.Create((1 << bitDepth) - 1));
Vector128<short> narrowed = Vector128.Narrow(highBitDepthReconstructed.GetLower(), highBitDepthReconstructed.GetUpper());
narrowed.StoreUnsafe(ref Unsafe.As<TSample, short>(ref destination));
}
}

12
src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseWalshHadamardTransformer.cs

@ -40,7 +40,7 @@ internal static class Av1InverseWalshHadamardTransformer
int writeStride,
int coefficientCount,
Span<int> workspace)
=> TransformAdd<byte, Av1InverseTransformOutputOperator<byte>>(
=> TransformAdd<byte, Av1Inverse2dTransformer.OutputOperator<byte>>(
coefficients,
readBuffer,
readStride,
@ -70,7 +70,7 @@ internal static class Av1InverseWalshHadamardTransformer
int coefficientCount,
Span<int> workspace,
int bitDepth)
=> TransformAdd<short, Av1InverseTransformOutputOperator<short>>(
=> TransformAdd<short, Av1Inverse2dTransformer.OutputOperator<short>>(
coefficients,
readBuffer,
readStride,
@ -93,7 +93,7 @@ internal static class Av1InverseWalshHadamardTransformer
Span<int> workspace,
int bitDepth)
where TSample : unmanaged
where TOutputOperator : struct, IAv1InverseTransformOutputOperator<TSample>
where TOutputOperator : struct, Av1Inverse2dTransformer.IAv1InverseTransformOutputOperator<TSample>
{
if (Vector128.IsHardwareAccelerated)
{
@ -116,7 +116,7 @@ internal static class Av1InverseWalshHadamardTransformer
int coefficientCount,
int bitDepth)
where TSample : unmanaged
where TOutputOperator : struct, IAv1InverseTransformOutputOperator<TSample>
where TOutputOperator : struct, Av1Inverse2dTransformer.IAv1InverseTransformOutputOperator<TSample>
{
ref int coefficientBase = ref MemoryMarshal.GetReference(coefficients);
Vector128<int> row0;
@ -172,7 +172,7 @@ internal static class Av1InverseWalshHadamardTransformer
Span<int> workspace,
int bitDepth)
where TSample : unmanaged
where TOutputOperator : struct, IAv1InverseTransformOutputOperator<TSample>
where TOutputOperator : struct, Av1Inverse2dTransformer.IAv1InverseTransformOutputOperator<TSample>
{
ref TSample readBase = ref MemoryMarshal.GetReference(readBuffer);
ref TSample writeBase = ref MemoryMarshal.GetReference(writeBuffer);
@ -300,7 +300,7 @@ internal static class Av1InverseWalshHadamardTransformer
Vector128<int> row3,
int bitDepth)
where TSample : unmanaged
where TOutputOperator : struct, IAv1InverseTransformOutputOperator<TSample>
where TOutputOperator : struct, Av1Inverse2dTransformer.IAv1InverseTransformOutputOperator<TSample>
{
ref TSample readBase = ref MemoryMarshal.GetReference(readBuffer);
ref TSample writeBase = ref MemoryMarshal.GetReference(writeBuffer);

21
src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1Adst16Forward1dOperator.cs

@ -1,21 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
/// <summary>
/// Defines the sixteen-point AV1 forward asymmetric discrete sine transform operator.
/// </summary>
internal readonly struct Av1Adst16Forward1dOperator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Adst16(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}

21
src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1Adst4Forward1dOperator.cs

@ -1,21 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
/// <summary>
/// Defines the four-point AV1 forward asymmetric discrete sine transform operator.
/// </summary>
internal readonly struct Av1Adst4Forward1dOperator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Adst4(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}

21
src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1Adst8Forward1dOperator.cs

@ -1,21 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
/// <summary>
/// Defines the eight-point AV1 forward asymmetric discrete sine transform operator.
/// </summary>
internal readonly struct Av1Adst8Forward1dOperator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Adst8(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}

21
src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1Dct16Forward1dOperator.cs

@ -1,21 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
/// <summary>
/// Defines the sixteen-point AV1 forward discrete cosine transform operator.
/// </summary>
internal readonly struct Av1Dct16Forward1dOperator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Dct16(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}

21
src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1Dct32Forward1dOperator.cs

@ -1,21 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
/// <summary>
/// Defines the thirty-two-point AV1 forward discrete cosine transform operator.
/// </summary>
internal readonly struct Av1Dct32Forward1dOperator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Dct32(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}

21
src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1Dct4Forward1dOperator.cs

@ -1,21 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
/// <summary>
/// Defines the four-point AV1 forward discrete cosine transform operator.
/// </summary>
internal readonly struct Av1Dct4Forward1dOperator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Dct4(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}

21
src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1Dct64Forward1dOperator.cs

@ -1,21 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
/// <summary>
/// Defines the sixty-four-point AV1 forward discrete cosine transform operator.
/// </summary>
internal readonly struct Av1Dct64Forward1dOperator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Dct64(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}

21
src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1Dct8Forward1dOperator.cs

@ -1,21 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
/// <summary>
/// Defines the eight-point AV1 forward discrete cosine transform operator.
/// </summary>
internal readonly struct Av1Dct8Forward1dOperator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Dct8(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}

21
src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1Identity16Forward1dOperator.cs

@ -1,21 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward;
/// <summary>
/// Defines the sixteen-point AV1 forward identity transform operator.
/// </summary>
internal readonly struct Av1Identity16Forward1dOperator : IAv1ForwardTransform1dOperator
{
/// <inheritdoc/>
public static void Transform<TValue>(
ref byte values,
nint inputStride,
nint outputStride,
ref Av1TransformVector<TValue> buffer0,
ref Av1TransformVector<TValue> buffer1,
int cosBit)
where TValue : struct
=> Av1ForwardTransformOperations.Identity16(ref values, inputStride, outputStride, ref buffer0, ref buffer1, cosBit);
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save