Browse Source

Complete AV1 selectable compound prediction

pull/2633/head
James Jackson-South 2 weeks ago
parent
commit
864b11ad97
  1. 23
      HEIF_IMPLEMENTATION_PLAN.md
  2. 80
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs
  3. 54
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContext.cs
  4. 72
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolContextHelper.cs
  5. 67
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs
  6. 92
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs
  7. 64
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeights.cs
  8. 501
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Blending.cs
  9. 294
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Masks.cs
  10. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.cs
  11. 204
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WedgeMask.cs
  12. 87
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs
  13. 249
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs
  14. 208
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs
  15. 329
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundInterPredictorTests.cs
  16. 81
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterFrameModeInfoTests.cs
  17. 49
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionModeInfoTests.cs
  18. 292
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs
  19. 154
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SelectableCompoundEntropyTests.cs
  20. 12
      tests/ImageSharp.Tests/TestImages.cs
  21. 32
      tests/Images/Input/Heif/Av1/Conformance/README.md
  22. 3
      tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-difference-weighted-compound-libaom.y4m
  23. 3
      tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-difference-weighted-compound-libavif.png
  24. 3
      tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-difference-weighted-compound.avif
  25. 3
      tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-distance-weighted-compound-libaom.y4m
  26. 3
      tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-distance-weighted-compound-libavif.png
  27. 3
      tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-distance-weighted-compound.avif
  28. 3
      tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-inter-intra-libaom.y4m
  29. 3
      tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-inter-intra-libavif.png
  30. 3
      tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-inter-intra.avif
  31. 3
      tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-wedge-compound-libaom.y4m
  32. 3
      tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-wedge-compound-libavif.png
  33. 3
      tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-wedge-compound.avif

23
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-29 against production checkpoint `8b9aa4b2cfe59ff0f8c348d9bb06678a5ff12f2a` and the verified equal-average compound work described below. Committed checkpoints include the AV1 transform architecture, OBU framing, intra-block copy, 12-profile reconstruction matrix, layered-item properties, layered reference/header/CDF/motion-field state, inter-frame intra blocks, SIMD-first translational prediction, and complete single-reference inter reconstruction. The current checkpoint adds compound reference trees and modes, paired reference-MV derivation, reference-dependent bounded sequence decoding, and allocation-free SIMD-first equal averaging. A pinned libavif/libaom 19-frame YUV444 sequence passes exact final native-plane and RGBA presentation comparison, production `DecodeBlock()` coverage, constrained allocation, and exactly-once allocator-return tracking. Both source target builds pass with zero warnings and errors; the test-project analyzer build passes with zero errors and only pre-existing repository warnings; 119 direct focused cases and three `FeatureTestRunner` cases pass with zero failures or skips; and `git diff --check` is clean. 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-29 against production checkpoint `e1a764b8f2e8a8db98a003fb3b913213f40084db` and the verified selectable-compound checkpoint awaiting commit. Committed checkpoints include the AV1 transform architecture, OBU framing, intra-block copy, 12-profile reconstruction matrix, layered-item properties, layered reference/header/CDF/motion-field state, inter-frame intra blocks, SIMD-first translational prediction, complete single-reference inter reconstruction, compound reference trees and modes, paired reference-MV derivation, reference-dependent bounded sequence decoding, and allocation-free SIMD-first equal averaging. The verified working tree additionally implements selected inter-intra prediction plus distance-weighted, wedge, and difference-weighted compound blending. Four pinned bounded sequences pass exact native-plane and presentation comparison, normative syntax and production reconstruction pass directly at 8/10/12 bit, SIMD-first mask and blend paths pass through `FeatureTestRunner`, and constrained allocation returns every tracked buffer exactly once. Both source targets build with zero warnings and errors; the `net10.0` test project builds with zero errors and its 1,013 pre-existing warnings; 25 direct focused cases and six `FeatureTestRunner` cases pass without failures or skips; the documentation review is complete; and `git diff --check` is clean. 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:
@ -39,15 +39,15 @@ Status meanings:
- **Not started:** supporting primitives may exist, but the production format path is absent.
- **Current:** the only work item that should be advanced before taking the next queued item.
Current development stage: **Stage 3 — complete AV1 still-image decoding.** The decoder retains reference/header/CDF/motion-field state, derives frame-level skip-mode references, consumes temporal segment prediction, decodes intra-coded blocks inside inter frames, reconstructs single-reference NEAREST/NEAR/NEW/GLOBAL translational blocks, and now reconstructs two retained predictors with equal-weight compound averaging before residual traversal. Exact dependent-frame and 19-frame compound-sequence native/presentation comparisons, constrained allocation, and ownership evidence pass. Selectable distance-weighted and masked compound blending, inter-intra prediction, OBMC, warped motion, scaled references, and non-translational global prediction remain explicitly unsupported. Neither AV1 nor HEVC production encoding is implemented.
Current development stage: **Stage 3 — complete AV1 still-image decoding.** The decoder retains reference/header/CDF/motion-field state, derives frame-level skip-mode references, consumes temporal segment prediction, decodes intra-coded blocks inside inter frames, reconstructs single-reference NEAREST/NEAR/NEW/GLOBAL translational blocks, and reconstructs equal-average, distance-weighted, wedge, difference-weighted, and inter-intra prediction before residual traversal. The selectable-compound checkpoint is verified and awaiting commit. OBMC, warped motion, scaled references, and non-translational global prediction remain explicitly unsupported. Neither AV1 nor HEVC production encoding is implemented.
Immediate checkpoint: **implement and independently verify selected inter-intra prediction and selectable compound blending before advancing to OBMC.** Preserve the committed translational predictor and equal-average rounding contracts, consume each selection tree in normative order, keep mask and predictor workspaces allocator-owned and allocation-free per block, and prove each production branch with independently encoded bounded AVIF content and exact native-plane comparison. This work remains inside the existing image-item and bounded image-sequence surfaces and must not expand into a general ISO BMFF/video model.
Immediate checkpoint: **commit the verified selected inter-intra and selectable compound checkpoint, then advance to OBMC.** Preserve the committed translational and compound-prediction contracts, keep predictor workspaces allocator-owned and allocation-free per block, and require independently encoded bounded AVIF content with exact native-plane comparison for each next production branch. This work remains inside the existing image-item and bounded image-sequence surfaces and must not expand 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 work 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, retained layered reference/header/CDF/motion-field state, temporal segment prediction, inter-frame intra blocks, SIMD-first translational prediction, exact single-reference inter reconstruction, and exact equal-average compound reconstruction through a reference-dependent bounded sequence. | Implement inter-intra and selectable compound blending, then OBMC, scaled-reference, warped, and non-translational global prediction; remove every other valid AV1 still-image unsupported branch with independent 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 work 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, retained layered reference/header/CDF/motion-field state, temporal segment prediction, inter-frame intra blocks, SIMD-first translational prediction, exact single-reference inter reconstruction, exact equal-average compound reconstruction, and verified selectable compound and inter-intra reconstruction through reference-dependent bounded sequences. | Commit the verified checkpoint, then implement OBMC, scaled-reference, warped, and non-translational global prediction; remove every other valid AV1 still-image unsupported branch with independent vectors; then complete the remaining HEVC profile and Range Extensions matrix. |
| 4 | Complete decoded presentation and animation | In progress | Shared SIMD-first AV1/HEVC color conversion, ICC application, grids, transforms, direct planar alpha composition, frame metadata, repetition, and independently decodable AV1 sequence samples exist. | Close the full color/ICC cross-product, HEVC sequence decoding, AV1/HEVC reference-dependent samples, frame-local metadata/alpha behavior, and independent animated decode vectors. |
| 5 | AV1/AVIF encoding | Not started | RGB-to-planar conversion, forward transforms, OBU writer foundations, options, and container-writing infrastructure exist. | `HeifEncoderCore` still rejects AV1. Implement a real independently decodable lossy/lossless AV1 payload and the complete AVIF item/metadata matrix. |
| 6 | HEVC/HEIC encoding | Not started | Shared input color conversion, options, and HEIF writer infrastructure exist. | `HeifEncoderCore` still rejects HEVC. Implement a real independently decodable lossy/lossless HEVC payload and the complete HEIC item/metadata matrix. |
@ -89,7 +89,7 @@ Immediate checkpoint: **implement and independently verify selected inter-intra
- [x] Correct the progressive dependent-frame extraction and compare the final frame's first three native planes with pinned libaom output and its final RGBA presentation with pinned libavif exactly. The unmodified AVIF has the recorded SHA-256 and stores the primary color item's 55-byte base extent at offset 511 and 17-byte dependent extent at offset 583. The corrected logical `.bit` payload decodes as two YUV444 frames with pinned libaom `03087864cf4bea6abb0d28f95cf7843511413d8f`; the retained two-frame YUV444-alpha reference and final PNG come from pinned libavif linked to that build. The production test selects the second native frame, requires inter-coded blocks, and passes exact native and presentation comparisons through `FeatureTestRunner`.
- [x] Decode all compound reference-pair trees and inter modes, retain paired spatial/temporal reference-MV candidates through DRL selection, and reconstruct two translational predictors with allocation-free equal-weight averaging across 8/10/12-bit samples without changing the single-reference predictor contract or rounding model.
- [x] Verify the equal-average production branch with the pinned 19-frame libavif YUV444 sequence. The final native Y, U, and V planes match pinned libaom exactly, final RGBA presentation matches pinned libavif exactly, production `DecodeBlock()` is covered at 8/10/12 bit, constrained plane allocation passes, and allocator tracking returns every retained and temporal motion-field buffer exactly once.
- [ ] Decode selected inter-intra prediction and selectable distance-weighted, wedge, and difference-weighted compound blending.
- [x] Decode selected inter-intra prediction and selectable distance-weighted, wedge, and difference-weighted compound blending. Normative-order syntax, direct 8/10/12-bit production reconstruction, SIMD/scalar arithmetic, four pinned bounded sequences, exact final native planes and presentation, constrained contiguous planes, and balanced allocator returns pass. Both source targets build with zero warnings and errors; the test project builds with zero errors and pre-existing repository warnings; 25 direct focused cases and six `FeatureTestRunner` cases pass without failures or skips; and `git diff --check` is clean.
- [ ] Decode OBMC, scaled references, warped motion, and non-translational global-motion prediction.
- [ ] Verify every remaining connected mode and filter with independently encoded bounded AV1 content 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.
@ -352,7 +352,7 @@ This table is intentionally incomplete. Add a row before each additional AV1 or
## Current implementation assessment
This assessment was reconciled with the source tree on 2026-08-29, including production checkpoint `8b9aa4b2cfe59ff0f8c348d9bb06678a5ff12f2a` and the verified equal-average compound checkpoint. Unless a result is stated explicitly, each item is a source-inspection finding rather than a verified interoperability claim.
This assessment was reconciled with the source tree on 2026-08-29, including production checkpoint `e1a764b8f2e8a8db98a003fb3b913213f40084db`. Unless a result is stated explicitly, each item is a source-inspection finding rather than a verified interoperability claim.
### Public integration
@ -391,7 +391,7 @@ This assessment was reconciled with the source tree on 2026-08-29, including pro
- The bounded `Av1Decoder` session parses all tile state before allocating and reconstructing each coded image layer or sequence sample. 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. Bounded image-sequence decoding accepts `show_existing_frame`, including its key-frame reference reset and separate film-grain presentation ownership, without exposing a general animation/video reference model.
- Committed inter-frame support parses single-reference selection, builds the fixed-capacity spatial and temporal reference-MV stack, decodes NEAREST/NEAR/NEW/GLOBAL and DRL syntax, decodes or infers interpolation filters, checks inter-intra and motion-mode eligibility, and invokes simple translational prediction before residual reconstruction. Commit `8b9aa4b2cfe59ff0f8c348d9bb06678a5ff12f2a` records exact corrected dependent-frame native-plane and final-presentation comparisons plus the complete focused Release matrix and ownership/allocation evidence.
- The verified equal-average compound checkpoint owns every compound reference-selection and inter-mode distribution through the retained CDF lifecycle, derives primary and secondary candidate vectors as inseparable pairs, reconstructs both retained references through the existing translational predictor, and combines them through one SIMD-first byte/ushort averaging operator with scalar parity. The pinned 19-frame libavif sequence exercises the complete production block branch and matches pinned libaom native planes and pinned libavif presentation exactly after decoding all preceding reference samples.
- Commit `e1a764b8f2e8a8db98a003fb3b913213f40084db` owns every compound reference-selection and inter-mode distribution through the retained CDF lifecycle, derives primary and secondary candidate vectors as inseparable pairs, reconstructs both retained references through the existing translational predictor, and combines them through one SIMD-first byte/ushort averaging operator with scalar parity. The pinned 19-frame libavif sequence exercises the complete production block branch and matches pinned libaom native planes and pinned libavif presentation exactly after decoding all preceding reference samples.
- Source inspection against pinned libaom `03087864cf4bea6abb0d28f95cf7843511413d8f` resolves two disputed audit claims in favor of the current predicates. The interpolation call path uses `is_nontrans_global_motion()`, which returns false only for `TRANSLATION`, so an identity `GLOBALMV` block omits filter symbols. The single-reference spatial extension loops use `MAX_MV_REF_CANDIDATES`, which is two, while the full reference-MV stack capacity is eight. These loops are spatial extension, not temporal extension.
- Every padded AV1 frame plane requests ImageSharp's contiguous allocation contract, and constrained-allocator reconstruction passes without copying. Motion fields use allocator-owned storage and counted leases; allocation tracking passes for initialization, retained aliases, failure unwinding, presentation ownership, decoder-result ownership, and exactly-once disposal. The corrected logical progressive color payload reaches the production inter branch and its final native planes match pinned libaom exactly.
- 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.
@ -422,7 +422,7 @@ This assessment was reconciled with the source tree on 2026-08-29, including pro
### 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. 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. The corrected progressive color item then verifies its production single-reference inter branch against exact pinned-libaom native planes and pinned-libavif presentation. The pinned 19-frame YUV444 sequence separately reaches production equal-average compound reconstruction after every preceding reference sample and matches its final native and presented references exactly. A real palette stream truncated inside its tile entropy payload verifies libaom-equivalent overflow/trailing-bit rejection and decoder-session recovery. The current compound checkpoint passes 119 direct focused cases plus three `FeatureTestRunner` cases, zero-warning `net10.0` and `net11.0` source builds, a zero-error test-project analyzer build with pre-existing repository warnings, allocator lifetime validation, and `git diff --check`. 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. The corrected progressive color item then verifies its production single-reference inter branch against exact pinned-libaom native planes and pinned-libavif presentation. The pinned 19-frame YUV444 sequence separately reaches production equal-average compound reconstruction after every preceding reference sample and matches its final native and presented references exactly. A real palette stream truncated inside its tile entropy payload verifies libaom-equivalent overflow/trailing-bit rejection and decoder-session recovery. The committed equal-average checkpoint passes 119 direct focused cases plus three `FeatureTestRunner` cases, zero-warning `net10.0` and `net11.0` source builds, a zero-error test-project analyzer build with pre-existing repository warnings, allocator lifetime validation, and `git diff --check`. Four additional pinned sequences reach distance-weighted, both wedge signs, both difference-mask orientations, and smooth and wedge inter-intra reconstruction with exact final native and presented output. The selectable-compound checkpoint passes both source targets, the test-project analyzer build, 25 direct focused cases, six `FeatureTestRunner` cases, constrained allocation, exactly-once tracked returns, and `git diff --check`. 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.
@ -569,7 +569,7 @@ Implement and verify in dependency order:
- [x] Correct the logical progressive color payload and compare the final dependent frame's native planes and libavif presentation exactly. Pinned libaom decodes both layers from the primary item's two `iloc` extents, and pinned libavif produces the retained two-frame YUV444-alpha reference and final PNG. The exact production-path comparisons pass through `FeatureTestRunner`.
- [x] Decode all compound reference trees and inter modes, derive and retain paired reference-MV candidates, reconstruct both unscaled translational references, and combine them with equal-weight averaging through `Vector512`, `Vector256`, `Vector128`, and scalar paths without per-block allocation.
- [x] Verify equal-average compound reconstruction with the pinned 19-frame libavif sequence, exact pinned-libaom final native planes, exact pinned-libavif final presentation, direct 8/10/12-bit production-block tests, constrained allocation, and exactly-once motion-field disposal. Both Release source targets pass with zero warnings and errors; the test-project analyzer build passes with zero errors and pre-existing repository warnings; 119 direct focused cases and three `FeatureTestRunner` cases pass without failures or skips; and `git diff --check` is clean.
- [ ] Decode and reconstruct selected inter-intra prediction and selectable distance-weighted, wedge, and difference-weighted compound blending through reconstructed reference planes.
- [x] Decode and reconstruct selected inter-intra prediction and selectable distance-weighted, wedge, and difference-weighted compound blending through reconstructed reference planes. Direct syntax and 8/10/12-bit production tests, SIMD/scalar mask and blend tests, exact four-sequence native/presentation comparisons, constrained contiguous allocation, and exactly-once tracked returns pass. Both source targets build with zero warnings and errors; the test project builds with zero errors and pre-existing repository warnings; 25 direct focused cases and six `FeatureTestRunner` cases pass without failures or skips; and `git diff --check` is clean.
- [ ] Decode and reconstruct OBMC, scaled references, warped motion, and non-translational global motion through reconstructed reference planes.
- [ ] Verify every remaining connected inter mode and filter with independently encoded bounded AV1 content and exact native-plane comparisons.
- [ ] Lossless and high-bit-depth reconstruction with correct clipping and intermediate precision.
@ -808,8 +808,9 @@ The dashboard and immediate execution queue define the remaining critical path.
- [x] Finish the Phase 8 checkpoint for the implemented AV1 `Vector512` transforms, including Release, feature-isolation, and benchmark evidence.
- [x] Finish the complete focused Release matrix, final source/documentation review, and checkpoint implementation for the simple single-reference inter slice. Commit `8b9aa4b2cfe59ff0f8c348d9bb06678a5ff12f2a` records the corrected real dependent-frame AVIF, exact pinned-libaom native-plane and pinned-libavif presentation comparisons, and 293 focused cases passing with zero failures or skips.
- [x] Implement compound reference selection and equal averaging with paired reference-MV derivation, allocation-free SIMD/scalar parity, and exact independent native/presentation evidence through a reference-dependent bounded AVIF sequence.
- [ ] **Current:** implement selected inter-intra prediction and selectable distance-weighted, wedge, and difference-weighted compound blending, then continue the remaining Phase 3 inter modes in dependency order: OBMC, scaled references, warped motion, and non-translational global prediction. Each mode requires independent exact evidence before the next begins.
- [x] Implement compound reference selection and equal averaging with paired reference-MV derivation, allocation-free SIMD/scalar parity, and exact independent native/presentation evidence through a reference-dependent bounded AVIF sequence. Commit `e1a764b8f2e8a8db98a003fb3b913213f40084db` records the checkpoint.
- [x] Implement and independently verify selected inter-intra prediction and selectable distance-weighted, wedge, and difference-weighted compound blending through exact pinned native-plane and presentation comparisons, direct 8/10/12-bit production coverage, constrained allocation, and SIMD/scalar parity.
- [ ] **Next after checkpoint commit:** implement OBMC, then continue the remaining Phase 3 inter modes in dependency order: scaled references, warped motion, and non-translational global prediction. Each mode requires independent exact evidence before the next begins.
- [ ] Remove every other unsupported valid AV1 still-image syntax path, correct the audited 12-bit inverse-transform arithmetic, and prove the complete AVIF decode matrix with independent inputs and scalar/SIMD parity.
- [ ] Close Phase 4 by completing the remaining HEVC profile and Range Extensions matrix with exact native-plane and presented-image evidence.
- [ ] Close Phase 5 and the decode portion of the bounded sequence ledger: color, ICC, alpha, grids, presentation transforms, reference-dependent samples, and complete animated AVIF/HEIC decode.

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

@ -114,6 +114,80 @@ internal static class Av1DefaultDistributions
/// </summary>
public static Av1Distribution[] InterIntra => [new(16384), new(26887), new(27597), new(30237)];
/// <summary>
/// Gets the four-symbol inter-intra mode distributions indexed by block-size group.
/// </summary>
public static Av1Distribution[] InterIntraMode =>
[
new(8192, 16384, 24576),
new(1875, 11082, 27332),
new(2473, 9996, 26388),
new(4238, 11537, 25926),
];
/// <summary>
/// Gets the inter-intra wedge-selection distributions indexed by block size.
/// </summary>
public static Av1Distribution[] WedgeInterIntra =>
[
new(16384), new(16384), new(16384), new(20036), new(24957), new(26704),
new(27530), new(29564), new(29444), new(26872), new(16384), new(16384),
new(16384), new(16384), new(16384), new(16384), new(16384), new(16384),
new(16384), new(16384), new(16384), new(16384),
];
/// <summary>
/// Gets the wedge-or-difference masked-compound distributions indexed by block size.
/// </summary>
public static Av1Distribution[] CompoundType =>
[
new(16384), new(16384), new(16384), new(23431), new(13171), new(11470),
new(9770), new(9100), new(8233), new(6172), new(16384), new(16384),
new(16384), new(16384), new(16384), new(16384), new(16384), new(16384),
new(11820), new(7701), new(16384), new(16384),
];
/// <summary>
/// Gets the sixteen-symbol wedge-index distributions indexed by block size.
/// </summary>
public static Av1Distribution[] WedgeIndex =>
[
CreateUniformWedgeIndexDistribution(),
CreateUniformWedgeIndexDistribution(),
CreateUniformWedgeIndexDistribution(),
new(2438, 4440, 6599, 8663, 11005, 12874, 15751, 18094, 20359, 22362, 24127, 25702, 27752, 29450, 31171),
new(806, 3266, 6005, 6738, 7218, 7367, 7771, 14588, 16323, 17367, 18452, 19422, 22839, 26127, 29629),
new(2779, 3738, 4683, 7213, 7775, 8017, 8655, 14357, 17939, 21332, 24520, 27470, 29456, 30529, 31656),
new(1684, 3625, 5675, 7108, 9302, 11274, 14429, 17144, 19163, 20961, 22884, 24471, 26719, 28714, 30877),
new(1142, 3491, 6277, 7314, 8089, 8355, 9023, 13624, 15369, 16730, 18114, 19313, 22521, 26012, 29550),
new(2742, 4195, 5727, 8035, 8980, 9336, 10146, 14124, 17270, 20533, 23434, 25972, 27944, 29570, 31416),
new(1727, 3948, 6101, 7796, 9841, 12344, 15766, 18944, 20638, 22038, 23963, 25311, 26988, 28766, 31012),
CreateUniformWedgeIndexDistribution(),
CreateUniformWedgeIndexDistribution(),
CreateUniformWedgeIndexDistribution(),
CreateUniformWedgeIndexDistribution(),
CreateUniformWedgeIndexDistribution(),
CreateUniformWedgeIndexDistribution(),
CreateUniformWedgeIndexDistribution(),
CreateUniformWedgeIndexDistribution(),
new(154, 987, 1925, 2051, 2088, 2111, 2151, 23033, 23703, 24284, 24985, 25684, 27259, 28883, 30911),
new(1135, 1322, 1493, 2635, 2696, 2737, 2770, 21016, 22935, 25057, 27251, 29173, 30089, 30960, 31933),
CreateUniformWedgeIndexDistribution(),
CreateUniformWedgeIndexDistribution(),
];
/// <summary>
/// Gets the average-or-distance-weighted compound distributions indexed by derived context.
/// </summary>
public static Av1Distribution[] CompoundIndex =>
[new(18244), new(12865), new(7053), new(13259), new(9334), new(4644)];
/// <summary>
/// Gets the unmasked-or-masked compound-group distributions indexed by derived context.
/// </summary>
public static Av1Distribution[] CompoundGroupIndex =>
[new(26607), new(22891), new(18840), new(24594), new(19934), new(22674)];
/// <summary>
/// Gets the Simple Translation, OBMC, or Warped motion-mode distributions indexed by block size.
/// </summary>
@ -2682,6 +2756,12 @@ internal static class Av1DefaultDistributions
public static Av1Distribution[][][] GetEndOfBlockExtra(int baseQIndex)
=> Av1Distribution.CreateCopy(EndOfBlockExtra[GetQContext(baseQIndex)]);
/// <summary>
/// Creates the uniform sixteen-symbol distribution used by block sizes that cannot signal a wedge index.
/// </summary>
private static Av1Distribution CreateUniformWedgeIndexDistribution()
=> new(2048, 4096, 6144, 8192, 10240, 12288, 14336, 16384, 18432, 20480, 22528, 24576, 26624, 28672, 30720);
/// <summary>
/// Maps a base quantizer index to one of the four AV1 coefficient-probability initialization bands.
/// </summary>

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

@ -95,6 +95,12 @@ internal sealed class Av1FrameEntropyContext
this.CompoundBackwardReference = Av1DefaultDistributions.CompoundBackwardReference;
this.InterCompoundMode = Av1DefaultDistributions.InterCompoundMode;
this.InterIntra = Av1DefaultDistributions.InterIntra;
this.InterIntraMode = Av1DefaultDistributions.InterIntraMode;
this.WedgeInterIntra = Av1DefaultDistributions.WedgeInterIntra;
this.CompoundType = Av1DefaultDistributions.CompoundType;
this.WedgeIndex = Av1DefaultDistributions.WedgeIndex;
this.CompoundIndex = Av1DefaultDistributions.CompoundIndex;
this.CompoundGroupIndex = Av1DefaultDistributions.CompoundGroupIndex;
this.MotionMode = Av1DefaultDistributions.MotionMode;
this.Obmc = Av1DefaultDistributions.Obmc;
this.SwitchableInterpolation = Av1DefaultDistributions.SwitchableInterpolation;
@ -164,6 +170,12 @@ internal sealed class Av1FrameEntropyContext
this.CompoundBackwardReference = Av1Distribution.CreateCopy(source.CompoundBackwardReference);
this.InterCompoundMode = Av1Distribution.CreateCopy(source.InterCompoundMode);
this.InterIntra = Av1Distribution.CreateCopy(source.InterIntra);
this.InterIntraMode = Av1Distribution.CreateCopy(source.InterIntraMode);
this.WedgeInterIntra = Av1Distribution.CreateCopy(source.WedgeInterIntra);
this.CompoundType = Av1Distribution.CreateCopy(source.CompoundType);
this.WedgeIndex = Av1Distribution.CreateCopy(source.WedgeIndex);
this.CompoundIndex = Av1Distribution.CreateCopy(source.CompoundIndex);
this.CompoundGroupIndex = Av1Distribution.CreateCopy(source.CompoundGroupIndex);
this.MotionMode = Av1Distribution.CreateCopy(source.MotionMode);
this.Obmc = Av1Distribution.CreateCopy(source.Obmc);
this.SwitchableInterpolation = Av1Distribution.CreateCopy(source.SwitchableInterpolation);
@ -332,6 +344,36 @@ internal sealed class Av1FrameEntropyContext
/// </summary>
public Av1Distribution[] InterIntra { get; }
/// <summary>
/// Gets the inter-intra mode distributions indexed by block-size group.
/// </summary>
public Av1Distribution[] InterIntraMode { get; }
/// <summary>
/// Gets the inter-intra wedge-selection distributions indexed by block size.
/// </summary>
public Av1Distribution[] WedgeInterIntra { get; }
/// <summary>
/// Gets the masked-compound type distributions indexed by block size.
/// </summary>
public Av1Distribution[] CompoundType { get; }
/// <summary>
/// Gets the wedge-index distributions indexed by block size.
/// </summary>
public Av1Distribution[] WedgeIndex { get; }
/// <summary>
/// Gets the average-or-distance-weighted compound distributions indexed by derived context.
/// </summary>
public Av1Distribution[] CompoundIndex { get; }
/// <summary>
/// Gets the unmasked-or-masked compound-group distributions indexed by derived context.
/// </summary>
public Av1Distribution[] CompoundGroupIndex { get; }
/// <summary>
/// Gets the three-way motion-mode distributions indexed by block size.
/// </summary>
@ -523,6 +565,12 @@ internal sealed class Av1FrameEntropyContext
CopyState(source.CompoundBackwardReference, this.CompoundBackwardReference);
CopyState(source.InterCompoundMode, this.InterCompoundMode);
CopyState(source.InterIntra, this.InterIntra);
CopyState(source.InterIntraMode, this.InterIntraMode);
CopyState(source.WedgeInterIntra, this.WedgeInterIntra);
CopyState(source.CompoundType, this.CompoundType);
CopyState(source.WedgeIndex, this.WedgeIndex);
CopyState(source.CompoundIndex, this.CompoundIndex);
CopyState(source.CompoundGroupIndex, this.CompoundGroupIndex);
CopyState(source.MotionMode, this.MotionMode);
CopyState(source.Obmc, this.Obmc);
CopyState(source.SwitchableInterpolation, this.SwitchableInterpolation);
@ -598,6 +646,12 @@ internal sealed class Av1FrameEntropyContext
ResetUpdateCounts(this.CompoundBackwardReference);
ResetUpdateCounts(this.InterCompoundMode);
ResetUpdateCounts(this.InterIntra);
ResetUpdateCounts(this.InterIntraMode);
ResetUpdateCounts(this.WedgeInterIntra);
ResetUpdateCounts(this.CompoundType);
ResetUpdateCounts(this.WedgeIndex);
ResetUpdateCounts(this.CompoundIndex);
ResetUpdateCounts(this.CompoundGroupIndex);
ResetUpdateCounts(this.MotionMode);
ResetUpdateCounts(this.Obmc);
ResetUpdateCounts(this.SwitchableInterpolation);

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
@ -188,6 +189,77 @@ internal static class Av1SymbolContextHelper
3, 5, 5, 5, 6
];
/// <summary>
/// Gets the context that selects unmasked or masked compound prediction.
/// </summary>
/// <param name="above">The above block, or <see langword="null"/> at a tile boundary.</param>
/// <param name="left">The left block, or <see langword="null"/> at a tile boundary.</param>
/// <returns>The context in the inclusive range zero through five.</returns>
public static int GetCompoundGroupIndexContext(Av1BlockModeInfo? above, Av1BlockModeInfo? left)
{
int aboveContext = 0;
if (above is not null)
{
aboveContext = above.ReferenceFrames[1] > Av1ReferenceFrameType.Intra
? above.CompoundGroupIndex ? 1 : 0
: above.ReferenceFrames[0] == Av1ReferenceFrameType.Alternate ? 3 : 0;
}
int leftContext = 0;
if (left is not null)
{
leftContext = left.ReferenceFrames[1] > Av1ReferenceFrameType.Intra
? left.CompoundGroupIndex ? 1 : 0
: left.ReferenceFrames[0] == Av1ReferenceFrameType.Alternate ? 3 : 0;
}
return Math.Min(5, aboveContext + leftContext);
}
/// <summary>
/// Gets the context that selects equal or distance-weighted compound prediction.
/// </summary>
/// <param name="orderHintInfo">The active order-hint modulo domain.</param>
/// <param name="frameHeader">The current frame and retained reference order hints.</param>
/// <param name="modeInfo">The compound block whose references are selected.</param>
/// <param name="above">The above block, or <see langword="null"/> at a tile boundary.</param>
/// <param name="left">The left block, or <see langword="null"/> at a tile boundary.</param>
/// <returns>The context in the inclusive range zero through five.</returns>
public static int GetCompoundIndexContext(
ObuOrderHintInfo orderHintInfo,
ObuFrameHeader frameHeader,
Av1BlockModeInfo modeInfo,
Av1BlockModeInfo? above,
Av1BlockModeInfo? left)
{
ReadOnlySpan<uint> referenceFrameIndices = frameHeader.GetReferenceFrameIndices();
ReadOnlySpan<uint> referenceOrderHints = frameHeader.GetReferenceOrderHints();
int primaryCanonicalIndex = (int)modeInfo.ReferenceFrames[0] - (int)Av1ReferenceFrameType.Last;
int secondaryCanonicalIndex = (int)modeInfo.ReferenceFrames[1] - (int)Av1ReferenceFrameType.Last;
uint primaryOrderHint = referenceOrderHints[(int)referenceFrameIndices[primaryCanonicalIndex]];
uint secondaryOrderHint = referenceOrderHints[(int)referenceFrameIndices[secondaryCanonicalIndex]];
int forwardDistance = Math.Abs(orderHintInfo.GetRelativeDistance(secondaryOrderHint, frameHeader.OrderHint));
int backwardDistance = Math.Abs(orderHintInfo.GetRelativeDistance(frameHeader.OrderHint, primaryOrderHint));
int aboveContext = 0;
if (above is not null)
{
aboveContext = above.ReferenceFrames[1] > Av1ReferenceFrameType.Intra
? above.CompoundIndex ? 1 : 0
: above.ReferenceFrames[0] == Av1ReferenceFrameType.Alternate ? 1 : 0;
}
int leftContext = 0;
if (left is not null)
{
leftContext = left.ReferenceFrames[1] > Av1ReferenceFrameType.Intra
? left.CompoundIndex ? 1 : 0
: left.ReferenceFrames[0] == Av1ReferenceFrameType.Alternate ? 1 : 0;
}
return aboveContext + leftContext + (forwardDistance == backwardDistance ? 3 : 0);
}
/// <summary>
/// Reduces a rectangular transform size to the square context used by transform-size distributions.
/// </summary>

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

@ -385,6 +385,73 @@ internal ref struct Av1SymbolDecoder
return r.ReadSymbol(this.context.InterIntra[sizeGroup]) != 0;
}
/// <summary>
/// Reads the intra mode blended into a selected inter-intra block.
/// </summary>
/// <param name="blockSize">The decoded block size that selects the mode distribution.</param>
/// <returns>The selected inter-intra mode.</returns>
public Av1InterIntraMode ReadInterIntraMode(Av1BlockSize blockSize)
{
int sizeGroup = blockSize.GetSizeGroup();
ref Av1SymbolReader r = ref this.reader;
return (Av1InterIntraMode)r.ReadSymbol(this.context.InterIntraMode[sizeGroup]);
}
/// <summary>
/// Reads whether a selected inter-intra block uses a wedge mask.
/// </summary>
/// <param name="blockSize">The decoded block size that selects the wedge flag distribution.</param>
/// <returns><see langword="true"/> when wedge blending is selected.</returns>
public bool ReadUseInterIntraWedge(Av1BlockSize blockSize)
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.context.WedgeInterIntra[(int)blockSize]) != 0;
}
/// <summary>
/// Reads a wedge-mask index.
/// </summary>
/// <param name="blockSize">The decoded block size that selects the wedge-index distribution.</param>
/// <returns>The wedge index in the inclusive range zero through fifteen.</returns>
public byte ReadWedgeIndex(Av1BlockSize blockSize)
{
ref Av1SymbolReader r = ref this.reader;
return (byte)r.ReadSymbol(this.context.WedgeIndex[(int)blockSize]);
}
/// <summary>
/// Reads whether a compound block uses the masked-compound mode group.
/// </summary>
/// <param name="context">The derived neighboring compound-group context.</param>
/// <returns><see langword="true"/> for masked compound prediction.</returns>
public bool ReadCompoundGroupIndex(int context)
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.context.CompoundGroupIndex[context]) != 0;
}
/// <summary>
/// Reads whether an unmasked compound block uses equal averaging.
/// </summary>
/// <param name="context">The derived neighboring compound-index context.</param>
/// <returns><see langword="true"/> for equal averaging; otherwise, distance-weighted blending.</returns>
public bool ReadCompoundIndex(int context)
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.context.CompoundIndex[context]) != 0;
}
/// <summary>
/// Reads the selected masked-compound type for a wedge-capable block.
/// </summary>
/// <param name="blockSize">The decoded block size that selects the masked-compound distribution.</param>
/// <returns>The selected wedge or difference-weighted compound type.</returns>
public Av1CompoundType ReadMaskedCompoundType(Av1BlockSize blockSize)
{
ref Av1SymbolReader r = ref this.reader;
return (Av1CompoundType)((int)Av1CompoundType.Wedge + r.ReadSymbol(this.context.CompoundType[(int)blockSize]));
}
/// <summary>
/// Reads the motion model selected for an eligible single-reference inter block.
/// </summary>

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

@ -106,6 +106,98 @@ internal class Av1PredictionDecoder
blockModeInfoColumnOffset,
blockModeInfoRowOffset);
/// <summary>
/// Builds the intra predictor for an 8-bit inter-intra plane block in separate caller-owned storage.
/// </summary>
public void DecodeInterIntra(
ref Av1PartitionInfo partitionInfo,
Av1Plane plane,
Av1TileInfo tileInfo,
Span<byte> referenceBuffer,
int referenceStride,
Span<byte> destination,
int destinationStride,
Av1BitDepth bitDepth)
=> this.DecodeInterIntraCore(
ref partitionInfo,
plane,
tileInfo,
referenceBuffer,
referenceStride,
destination,
destinationStride,
bitDepth);
/// <summary>
/// Builds the intra predictor for a high-bit-depth inter-intra plane block in separate caller-owned storage.
/// </summary>
public void DecodeInterIntra(
ref Av1PartitionInfo partitionInfo,
Av1Plane plane,
Av1TileInfo tileInfo,
Span<short> referenceBuffer,
int referenceStride,
Span<short> destination,
int destinationStride,
Av1BitDepth bitDepth)
=> this.DecodeInterIntraCore(
ref partitionInfo,
plane,
tileInfo,
referenceBuffer,
referenceStride,
destination,
destinationStride,
bitDepth);
/// <summary>
/// Builds an inter-intra predictor from reconstructed frame neighbors without replacing those references.
/// </summary>
private void DecodeInterIntraCore<T>(
ref Av1PartitionInfo partitionInfo,
Av1Plane plane,
Av1TileInfo tileInfo,
Span<T> referenceBuffer,
int referenceStride,
Span<T> destination,
int destinationStride,
Av1BitDepth bitDepth)
where T : unmanaged, IBinaryInteger<T>
{
ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig;
int subX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0;
int subY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0;
Av1BlockSize planeBlockSize = partitionInfo.ModeInfo.BlockSize.GetSubsampled(subX, subY);
Av1TransformSize transformSize = planeBlockSize.GetMaximumTransformSize();
Av1PredictionMode mode = partitionInfo.ModeInfo.InterIntraMode switch
{
Av1InterIntraMode.Vertical => Av1PredictionMode.Vertical,
Av1InterIntraMode.Horizontal => Av1PredictionMode.Horizontal,
Av1InterIntraMode.Smooth => Av1PredictionMode.Smooth,
_ => Av1PredictionMode.DC,
};
Span<T> topNeighbor = referenceBuffer;
ReadOnlySpan<T> leftNeighbor = referenceBuffer[(referenceStride - 1)..];
// Libaom predicts one maximum-transform-sized plane block for inter-intra. Destination storage is separate
// because the inter predictor must remain intact until the final mask blend consumes both complete blocks.
this.PredictIntraBlock(
ref partitionInfo,
plane,
transformSize,
tileInfo,
destination,
destinationStride,
topNeighbor,
leftNeighbor,
referenceStride,
mode,
blockModeInfoColumnOffset: 0,
blockModeInfoRowOffset: 0,
bitDepth);
}
/// <summary>
/// Reconstructs a 10-bit or 12-bit intra-predicted transform block.
/// </summary>

64
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeights.cs

@ -0,0 +1,64 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
/// <summary>
/// Derives AV1 compound weights from retained reference display distances.
/// </summary>
internal static class Av1CompoundDistanceWeights
{
private const int MaximumFrameDistance = 31;
/// <summary>
/// Derives the weights applied to the first and second predictors.
/// </summary>
public static void Derive(
ObuOrderHintInfo orderHintInfo,
ObuFrameHeader frameHeader,
Av1ReferenceFrameType firstReference,
Av1ReferenceFrameType secondReference,
out int firstWeight,
out int secondWeight)
{
ReadOnlySpan<int> quantizedDistanceWeights = [2, 3, 2, 5, 2, 7, 1, MaximumFrameDistance];
ReadOnlySpan<int> quantizedDistanceLookup = [9, 7, 11, 5, 12, 4, 13, 3];
ReadOnlySpan<uint> referenceFrameIndices = frameHeader.GetReferenceFrameIndices();
ReadOnlySpan<uint> referenceOrderHints = frameHeader.GetReferenceOrderHints();
int firstCanonicalIndex = (int)firstReference - (int)Av1ReferenceFrameType.Last;
int secondCanonicalIndex = (int)secondReference - (int)Av1ReferenceFrameType.Last;
uint firstOrderHint = referenceOrderHints[(int)referenceFrameIndices[firstCanonicalIndex]];
uint secondOrderHint = referenceOrderHints[(int)referenceFrameIndices[secondCanonicalIndex]];
int secondDistance = Av1Math.Clip3(
0,
MaximumFrameDistance,
Math.Abs(orderHintInfo.GetRelativeDistance(secondOrderHint, frameHeader.OrderHint)));
int firstDistance = Av1Math.Clip3(
0,
MaximumFrameDistance,
Math.Abs(orderHintInfo.GetRelativeDistance(frameHeader.OrderHint, firstOrderHint)));
int order = secondDistance <= firstDistance ? 1 : 0;
int weightClass = 3;
if (secondDistance != 0 && firstDistance != 0)
{
for (weightClass = 0; weightClass < 3; weightClass++)
{
int secondScaledDistance = secondDistance * quantizedDistanceWeights[(weightClass * 2) + order];
int firstScaledDistance = firstDistance * quantizedDistanceWeights[(weightClass * 2) + (1 - order)];
if ((secondDistance > firstDistance && secondScaledDistance < firstScaledDistance) ||
(secondDistance <= firstDistance && secondScaledDistance > firstScaledDistance))
{
break;
}
}
}
firstWeight = quantizedDistanceLookup[(weightClass * 2) + order];
secondWeight = quantizedDistanceLookup[(weightClass * 2) + (1 - order)];
}
}

501
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Blending.cs

@ -0,0 +1,501 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
/// <content>
/// Provides distance-weighted and per-sample masked compound blending.
/// </content>
internal static partial class Av1CompoundInterPredictor
{
private const int DistanceWeightBits = 4;
private const int MaskWeightBits = 6;
private const int MaximumMaskAlpha = 1 << MaskWeightBits;
/// <summary>
/// Combines two 8-bit predictors with AV1 display-distance weights.
/// </summary>
public static void DistanceWeighted(
Span<byte> destination,
int destinationStride,
ReadOnlySpan<byte> second,
int secondStride,
int width,
int height,
int firstWeight,
int secondWeight)
{
for (int row = 0; row < height; row++)
{
Span<byte> destinationRow = destination.Slice(row * destinationStride, width);
ReadOnlySpan<byte> secondRow = second.Slice(row * secondStride, width);
ref byte destinationReference = ref MemoryMarshal.GetReference(destinationRow);
ref byte secondReference = ref MemoryMarshal.GetReference(secondRow);
int column = 0;
if (Vector512.IsHardwareAccelerated)
{
int vectorEnd = width - Vector512<byte>.Count;
for (; column <= vectorEnd; column += Vector512<byte>.Count)
{
Vector512<byte> firstVector = Vector512.LoadUnsafe(ref destinationReference, (nuint)column);
Vector512<byte> secondVector = Vector512.LoadUnsafe(ref secondReference, (nuint)column);
DistanceWeighted(firstVector, secondVector, firstWeight, secondWeight).StoreUnsafe(ref destinationReference, (nuint)column);
}
}
if (Vector256.IsHardwareAccelerated)
{
int vectorEnd = width - Vector256<byte>.Count;
for (; column <= vectorEnd; column += Vector256<byte>.Count)
{
Vector256<byte> firstVector = Vector256.LoadUnsafe(ref destinationReference, (nuint)column);
Vector256<byte> secondVector = Vector256.LoadUnsafe(ref secondReference, (nuint)column);
DistanceWeighted(firstVector, secondVector, firstWeight, secondWeight).StoreUnsafe(ref destinationReference, (nuint)column);
}
}
if (Vector128.IsHardwareAccelerated)
{
int vectorEnd = width - Vector128<byte>.Count;
for (; column <= vectorEnd; column += Vector128<byte>.Count)
{
Vector128<byte> firstVector = Vector128.LoadUnsafe(ref destinationReference, (nuint)column);
Vector128<byte> secondVector = Vector128.LoadUnsafe(ref secondReference, (nuint)column);
DistanceWeighted(firstVector, secondVector, firstWeight, secondWeight).StoreUnsafe(ref destinationReference, (nuint)column);
}
}
for (; column < width; column++)
{
destinationRow[column] = (byte)(((destinationRow[column] * firstWeight) + (secondRow[column] * secondWeight) + 8) >> DistanceWeightBits);
}
}
}
/// <summary>
/// Combines two high-bit-depth predictors with AV1 display-distance weights.
/// </summary>
public static void DistanceWeighted(
Span<ushort> destination,
int destinationStride,
ReadOnlySpan<ushort> second,
int secondStride,
int width,
int height,
int firstWeight,
int secondWeight)
{
for (int row = 0; row < height; row++)
{
Span<ushort> destinationRow = destination.Slice(row * destinationStride, width);
ReadOnlySpan<ushort> secondRow = second.Slice(row * secondStride, width);
ref ushort destinationReference = ref MemoryMarshal.GetReference(destinationRow);
ref ushort secondReference = ref MemoryMarshal.GetReference(secondRow);
int column = 0;
if (Vector512.IsHardwareAccelerated)
{
int vectorEnd = width - Vector512<ushort>.Count;
for (; column <= vectorEnd; column += Vector512<ushort>.Count)
{
Vector512<ushort> firstVector = Vector512.LoadUnsafe(ref destinationReference, (nuint)column);
Vector512<ushort> secondVector = Vector512.LoadUnsafe(ref secondReference, (nuint)column);
DistanceWeighted(firstVector, secondVector, firstWeight, secondWeight).StoreUnsafe(ref destinationReference, (nuint)column);
}
}
if (Vector256.IsHardwareAccelerated)
{
int vectorEnd = width - Vector256<ushort>.Count;
for (; column <= vectorEnd; column += Vector256<ushort>.Count)
{
Vector256<ushort> firstVector = Vector256.LoadUnsafe(ref destinationReference, (nuint)column);
Vector256<ushort> secondVector = Vector256.LoadUnsafe(ref secondReference, (nuint)column);
DistanceWeighted(firstVector, secondVector, firstWeight, secondWeight).StoreUnsafe(ref destinationReference, (nuint)column);
}
}
if (Vector128.IsHardwareAccelerated)
{
int vectorEnd = width - Vector128<ushort>.Count;
for (; column <= vectorEnd; column += Vector128<ushort>.Count)
{
Vector128<ushort> firstVector = Vector128.LoadUnsafe(ref destinationReference, (nuint)column);
Vector128<ushort> secondVector = Vector128.LoadUnsafe(ref secondReference, (nuint)column);
DistanceWeighted(firstVector, secondVector, firstWeight, secondWeight).StoreUnsafe(ref destinationReference, (nuint)column);
}
}
for (; column < width; column++)
{
destinationRow[column] = (ushort)(((destinationRow[column] * firstWeight) + (secondRow[column] * secondWeight) + 8) >> DistanceWeightBits);
}
}
}
/// <summary>
/// Blends two 8-bit predictors through a contiguous AV1 alpha mask.
/// </summary>
public static void Blend(
Span<byte> destination,
int destinationStride,
ReadOnlySpan<byte> second,
int secondStride,
ReadOnlySpan<byte> mask,
int maskStride,
int width,
int height)
{
for (int row = 0; row < height; row++)
{
Span<byte> destinationRow = destination.Slice(row * destinationStride, width);
ReadOnlySpan<byte> secondRow = second.Slice(row * secondStride, width);
ReadOnlySpan<byte> maskRow = mask.Slice(row * maskStride, width);
ref byte destinationReference = ref MemoryMarshal.GetReference(destinationRow);
ref byte secondReference = ref MemoryMarshal.GetReference(secondRow);
ref byte maskReference = ref MemoryMarshal.GetReference(maskRow);
int column = 0;
if (Vector512.IsHardwareAccelerated)
{
int vectorEnd = width - Vector512<byte>.Count;
for (; column <= vectorEnd; column += Vector512<byte>.Count)
{
Vector512<byte> firstVector = Vector512.LoadUnsafe(ref destinationReference, (nuint)column);
Vector512<byte> secondVector = Vector512.LoadUnsafe(ref secondReference, (nuint)column);
Vector512<byte> maskVector = Vector512.LoadUnsafe(ref maskReference, (nuint)column);
Blend(firstVector, secondVector, maskVector).StoreUnsafe(ref destinationReference, (nuint)column);
}
}
if (Vector256.IsHardwareAccelerated)
{
int vectorEnd = width - Vector256<byte>.Count;
for (; column <= vectorEnd; column += Vector256<byte>.Count)
{
Vector256<byte> firstVector = Vector256.LoadUnsafe(ref destinationReference, (nuint)column);
Vector256<byte> secondVector = Vector256.LoadUnsafe(ref secondReference, (nuint)column);
Vector256<byte> maskVector = Vector256.LoadUnsafe(ref maskReference, (nuint)column);
Blend(firstVector, secondVector, maskVector).StoreUnsafe(ref destinationReference, (nuint)column);
}
}
if (Vector128.IsHardwareAccelerated)
{
int vectorEnd = width - Vector128<byte>.Count;
for (; column <= vectorEnd; column += Vector128<byte>.Count)
{
Vector128<byte> firstVector = Vector128.LoadUnsafe(ref destinationReference, (nuint)column);
Vector128<byte> secondVector = Vector128.LoadUnsafe(ref secondReference, (nuint)column);
Vector128<byte> maskVector = Vector128.LoadUnsafe(ref maskReference, (nuint)column);
Blend(firstVector, secondVector, maskVector).StoreUnsafe(ref destinationReference, (nuint)column);
}
}
for (; column < width; column++)
{
int alpha = maskRow[column];
destinationRow[column] = (byte)(((alpha * destinationRow[column]) + ((MaximumMaskAlpha - alpha) * secondRow[column]) + 32) >> MaskWeightBits);
}
}
}
/// <summary>
/// Blends two high-bit-depth predictors through a contiguous AV1 alpha mask.
/// </summary>
public static void Blend(
Span<ushort> destination,
int destinationStride,
ReadOnlySpan<ushort> second,
int secondStride,
ReadOnlySpan<byte> mask,
int maskStride,
int width,
int height)
{
for (int row = 0; row < height; row++)
{
Span<ushort> destinationRow = destination.Slice(row * destinationStride, width);
ReadOnlySpan<ushort> secondRow = second.Slice(row * secondStride, width);
ReadOnlySpan<byte> maskRow = mask.Slice(row * maskStride, width);
ref ushort destinationReference = ref MemoryMarshal.GetReference(destinationRow);
ref ushort secondReference = ref MemoryMarshal.GetReference(secondRow);
ref byte maskReference = ref MemoryMarshal.GetReference(maskRow);
int column = 0;
if (Vector512.IsHardwareAccelerated)
{
int vectorEnd = width - Vector512<ushort>.Count;
for (; column <= vectorEnd; column += Vector512<ushort>.Count)
{
Vector512<ushort> firstVector = Vector512.LoadUnsafe(ref destinationReference, (nuint)column);
Vector512<ushort> secondVector = Vector512.LoadUnsafe(ref secondReference, (nuint)column);
Vector512<ushort> maskVector = LoadMask512(ref maskReference, column);
Blend(firstVector, secondVector, maskVector).StoreUnsafe(ref destinationReference, (nuint)column);
}
}
if (Vector256.IsHardwareAccelerated)
{
int vectorEnd = width - Vector256<ushort>.Count;
for (; column <= vectorEnd; column += Vector256<ushort>.Count)
{
Vector256<ushort> firstVector = Vector256.LoadUnsafe(ref destinationReference, (nuint)column);
Vector256<ushort> secondVector = Vector256.LoadUnsafe(ref secondReference, (nuint)column);
Vector256<ushort> maskVector = LoadMask256(ref maskReference, column);
Blend(firstVector, secondVector, maskVector).StoreUnsafe(ref destinationReference, (nuint)column);
}
}
if (Vector128.IsHardwareAccelerated)
{
int vectorEnd = width - Vector128<ushort>.Count;
for (; column <= vectorEnd; column += Vector128<ushort>.Count)
{
Vector128<ushort> firstVector = Vector128.LoadUnsafe(ref destinationReference, (nuint)column);
Vector128<ushort> secondVector = Vector128.LoadUnsafe(ref secondReference, (nuint)column);
Vector128<ushort> maskVector = LoadMask128(ref maskReference, column);
Blend(firstVector, secondVector, maskVector).StoreUnsafe(ref destinationReference, (nuint)column);
}
}
for (; column < width; column++)
{
int alpha = maskRow[column];
destinationRow[column] = (ushort)(((alpha * destinationRow[column]) + ((MaximumMaskAlpha - alpha) * secondRow[column]) + 32) >> MaskWeightBits);
}
}
}
/// <summary>
/// Combines two 8-bit predictors with display-distance weights without explicit hardware intrinsics.
/// </summary>
public static void DistanceWeightedScalar(
Span<byte> destination,
int destinationStride,
ReadOnlySpan<byte> second,
int secondStride,
int width,
int height,
int firstWeight,
int secondWeight)
{
for (int row = 0; row < height; row++)
{
Span<byte> destinationRow = destination.Slice(row * destinationStride, width);
ReadOnlySpan<byte> secondRow = second.Slice(row * secondStride, width);
for (int column = 0; column < width; column++)
{
destinationRow[column] = (byte)(((destinationRow[column] * firstWeight) + (secondRow[column] * secondWeight) + 8) >> DistanceWeightBits);
}
}
}
/// <summary>
/// Blends two 8-bit predictors through an alpha mask without explicit hardware intrinsics.
/// </summary>
public static void BlendScalar(
Span<byte> destination,
int destinationStride,
ReadOnlySpan<byte> second,
int secondStride,
ReadOnlySpan<byte> mask,
int maskStride,
int width,
int height)
{
for (int row = 0; row < height; row++)
{
Span<byte> destinationRow = destination.Slice(row * destinationStride, width);
ReadOnlySpan<byte> secondRow = second.Slice(row * secondStride, width);
ReadOnlySpan<byte> maskRow = mask.Slice(row * maskStride, width);
for (int column = 0; column < width; column++)
{
int alpha = maskRow[column];
destinationRow[column] = (byte)(((alpha * destinationRow[column]) + ((MaximumMaskAlpha - alpha) * secondRow[column]) + 32) >> MaskWeightBits);
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> DistanceWeighted(Vector128<byte> first, Vector128<byte> second, int firstWeight, int secondWeight)
{
Av1IntraPredictorBase.Widen(first, out Vector128<int> first0, out Vector128<int> first1, out Vector128<int> first2, out Vector128<int> first3);
Av1IntraPredictorBase.Widen(second, out Vector128<int> second0, out Vector128<int> second1, out Vector128<int> second2, out Vector128<int> second3);
return Av1IntraPredictorBase.Narrow(
DistanceWeighted(first0, second0, firstWeight, secondWeight),
DistanceWeighted(first1, second1, firstWeight, secondWeight),
DistanceWeighted(first2, second2, firstWeight, secondWeight),
DistanceWeighted(first3, second3, firstWeight, secondWeight));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<byte> DistanceWeighted(Vector256<byte> first, Vector256<byte> second, int firstWeight, int secondWeight)
{
Av1IntraPredictorBase.Widen(first, out Vector256<int> first0, out Vector256<int> first1, out Vector256<int> first2, out Vector256<int> first3);
Av1IntraPredictorBase.Widen(second, out Vector256<int> second0, out Vector256<int> second1, out Vector256<int> second2, out Vector256<int> second3);
return Av1IntraPredictorBase.Narrow(
DistanceWeighted(first0, second0, firstWeight, secondWeight),
DistanceWeighted(first1, second1, firstWeight, secondWeight),
DistanceWeighted(first2, second2, firstWeight, secondWeight),
DistanceWeighted(first3, second3, firstWeight, secondWeight));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<byte> DistanceWeighted(Vector512<byte> first, Vector512<byte> second, int firstWeight, int secondWeight)
{
Av1IntraPredictorBase.Widen(first, out Vector512<int> first0, out Vector512<int> first1, out Vector512<int> first2, out Vector512<int> first3);
Av1IntraPredictorBase.Widen(second, out Vector512<int> second0, out Vector512<int> second1, out Vector512<int> second2, out Vector512<int> second3);
return Av1IntraPredictorBase.Narrow(
DistanceWeighted(first0, second0, firstWeight, secondWeight),
DistanceWeighted(first1, second1, firstWeight, secondWeight),
DistanceWeighted(first2, second2, firstWeight, secondWeight),
DistanceWeighted(first3, second3, firstWeight, secondWeight));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<ushort> DistanceWeighted(Vector128<ushort> first, Vector128<ushort> second, int firstWeight, int secondWeight)
{
Av1IntraPredictorBase.Widen(first.AsInt16(), out Vector128<int> first0, out Vector128<int> first1);
Av1IntraPredictorBase.Widen(second.AsInt16(), out Vector128<int> second0, out Vector128<int> second1);
return Av1IntraPredictorBase.Narrow(
DistanceWeighted(first0, second0, firstWeight, secondWeight),
DistanceWeighted(first1, second1, firstWeight, secondWeight)).AsUInt16();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<ushort> DistanceWeighted(Vector256<ushort> first, Vector256<ushort> second, int firstWeight, int secondWeight)
{
Av1IntraPredictorBase.Widen(first.AsInt16(), out Vector256<int> first0, out Vector256<int> first1);
Av1IntraPredictorBase.Widen(second.AsInt16(), out Vector256<int> second0, out Vector256<int> second1);
return Av1IntraPredictorBase.Narrow(
DistanceWeighted(first0, second0, firstWeight, secondWeight),
DistanceWeighted(first1, second1, firstWeight, secondWeight)).AsUInt16();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<ushort> DistanceWeighted(Vector512<ushort> first, Vector512<ushort> second, int firstWeight, int secondWeight)
{
Av1IntraPredictorBase.Widen(first.AsInt16(), out Vector512<int> first0, out Vector512<int> first1);
Av1IntraPredictorBase.Widen(second.AsInt16(), out Vector512<int> second0, out Vector512<int> second1);
return Av1IntraPredictorBase.Narrow(
DistanceWeighted(first0, second0, firstWeight, secondWeight),
DistanceWeighted(first1, second1, firstWeight, secondWeight)).AsUInt16();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<int> DistanceWeighted(Vector128<int> first, Vector128<int> second, int firstWeight, int secondWeight)
=> ((first * Vector128.Create(firstWeight)) + (second * Vector128.Create(secondWeight)) + Vector128.Create(8)) >> DistanceWeightBits;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<int> DistanceWeighted(Vector256<int> first, Vector256<int> second, int firstWeight, int secondWeight)
=> ((first * Vector256.Create(firstWeight)) + (second * Vector256.Create(secondWeight)) + Vector256.Create(8)) >> DistanceWeightBits;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<int> DistanceWeighted(Vector512<int> first, Vector512<int> second, int firstWeight, int secondWeight)
=> ((first * Vector512.Create(firstWeight)) + (second * Vector512.Create(secondWeight)) + Vector512.Create(8)) >> DistanceWeightBits;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> Blend(Vector128<byte> first, Vector128<byte> second, Vector128<byte> mask)
{
Av1IntraPredictorBase.Widen(first, out Vector128<int> first0, out Vector128<int> first1, out Vector128<int> first2, out Vector128<int> first3);
Av1IntraPredictorBase.Widen(second, out Vector128<int> second0, out Vector128<int> second1, out Vector128<int> second2, out Vector128<int> second3);
Av1IntraPredictorBase.Widen(mask, out Vector128<int> mask0, out Vector128<int> mask1, out Vector128<int> mask2, out Vector128<int> mask3);
return Av1IntraPredictorBase.Narrow(
Blend(first0, second0, mask0),
Blend(first1, second1, mask1),
Blend(first2, second2, mask2),
Blend(first3, second3, mask3));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<byte> Blend(Vector256<byte> first, Vector256<byte> second, Vector256<byte> mask)
{
Av1IntraPredictorBase.Widen(first, out Vector256<int> first0, out Vector256<int> first1, out Vector256<int> first2, out Vector256<int> first3);
Av1IntraPredictorBase.Widen(second, out Vector256<int> second0, out Vector256<int> second1, out Vector256<int> second2, out Vector256<int> second3);
Av1IntraPredictorBase.Widen(mask, out Vector256<int> mask0, out Vector256<int> mask1, out Vector256<int> mask2, out Vector256<int> mask3);
return Av1IntraPredictorBase.Narrow(
Blend(first0, second0, mask0),
Blend(first1, second1, mask1),
Blend(first2, second2, mask2),
Blend(first3, second3, mask3));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<byte> Blend(Vector512<byte> first, Vector512<byte> second, Vector512<byte> mask)
{
Av1IntraPredictorBase.Widen(first, out Vector512<int> first0, out Vector512<int> first1, out Vector512<int> first2, out Vector512<int> first3);
Av1IntraPredictorBase.Widen(second, out Vector512<int> second0, out Vector512<int> second1, out Vector512<int> second2, out Vector512<int> second3);
Av1IntraPredictorBase.Widen(mask, out Vector512<int> mask0, out Vector512<int> mask1, out Vector512<int> mask2, out Vector512<int> mask3);
return Av1IntraPredictorBase.Narrow(
Blend(first0, second0, mask0),
Blend(first1, second1, mask1),
Blend(first2, second2, mask2),
Blend(first3, second3, mask3));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<ushort> Blend(Vector128<ushort> first, Vector128<ushort> second, Vector128<ushort> mask)
{
Av1IntraPredictorBase.Widen(first.AsInt16(), out Vector128<int> first0, out Vector128<int> first1);
Av1IntraPredictorBase.Widen(second.AsInt16(), out Vector128<int> second0, out Vector128<int> second1);
Av1IntraPredictorBase.Widen(mask.AsInt16(), out Vector128<int> mask0, out Vector128<int> mask1);
return Av1IntraPredictorBase.Narrow(Blend(first0, second0, mask0), Blend(first1, second1, mask1)).AsUInt16();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<ushort> Blend(Vector256<ushort> first, Vector256<ushort> second, Vector256<ushort> mask)
{
Av1IntraPredictorBase.Widen(first.AsInt16(), out Vector256<int> first0, out Vector256<int> first1);
Av1IntraPredictorBase.Widen(second.AsInt16(), out Vector256<int> second0, out Vector256<int> second1);
Av1IntraPredictorBase.Widen(mask.AsInt16(), out Vector256<int> mask0, out Vector256<int> mask1);
return Av1IntraPredictorBase.Narrow(Blend(first0, second0, mask0), Blend(first1, second1, mask1)).AsUInt16();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<ushort> Blend(Vector512<ushort> first, Vector512<ushort> second, Vector512<ushort> mask)
{
Av1IntraPredictorBase.Widen(first.AsInt16(), out Vector512<int> first0, out Vector512<int> first1);
Av1IntraPredictorBase.Widen(second.AsInt16(), out Vector512<int> second0, out Vector512<int> second1);
Av1IntraPredictorBase.Widen(mask.AsInt16(), out Vector512<int> mask0, out Vector512<int> mask1);
return Av1IntraPredictorBase.Narrow(Blend(first0, second0, mask0), Blend(first1, second1, mask1)).AsUInt16();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<int> Blend(Vector128<int> first, Vector128<int> second, Vector128<int> mask)
=> ((mask * first) + ((Vector128.Create(MaximumMaskAlpha) - mask) * second) + Vector128.Create(32)) >> MaskWeightBits;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<int> Blend(Vector256<int> first, Vector256<int> second, Vector256<int> mask)
=> ((mask * first) + ((Vector256.Create(MaximumMaskAlpha) - mask) * second) + Vector256.Create(32)) >> MaskWeightBits;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<int> Blend(Vector512<int> first, Vector512<int> second, Vector512<int> mask)
=> ((mask * first) + ((Vector512.Create(MaximumMaskAlpha) - mask) * second) + Vector512.Create(32)) >> MaskWeightBits;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<ushort> LoadMask128(ref byte source, int offset)
{
Vector64<byte> packed = Unsafe.As<byte, Vector64<byte>>(ref Unsafe.Add(ref source, offset));
return Vector128.WidenLower(Vector128.Create(packed, Vector64<byte>.Zero));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<ushort> LoadMask256(ref byte source, int offset)
{
Vector128<byte> packed = Vector128.LoadUnsafe(ref source, (nuint)offset);
return Vector256.WidenLower(Vector256.Create(packed, Vector128<byte>.Zero));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<ushort> LoadMask512(ref byte source, int offset)
{
Vector256<byte> packed = Vector256.LoadUnsafe(ref source, (nuint)offset);
return Vector512.WidenLower(Vector512.Create(packed, Vector256<byte>.Zero));
}
}

294
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Masks.cs

@ -0,0 +1,294 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
/// <content>
/// Produces the smooth inter-intra and predictor-difference masks used by compound blending.
/// </content>
internal static partial class Av1CompoundInterPredictor
{
/// <summary>
/// Gets libaom's one-dimensional inter-intra alpha curve.
/// </summary>
private static ReadOnlySpan<byte> InterIntraWeights =>
[
60, 58, 56, 54, 52, 50, 48, 47, 45, 44, 42, 41, 39, 38, 37, 35,
34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 22, 21, 20,
19, 19, 18, 18, 17, 16, 16, 15, 15, 14, 14, 13, 13, 12, 12, 12,
11, 11, 10, 10, 10, 9, 9, 9, 8, 8, 8, 8, 7, 7, 7, 7,
6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
];
/// <summary>
/// Fills a smooth inter-intra mask for one plane.
/// </summary>
public static void FillInterIntraMask(
Span<byte> mask,
int maskStride,
int width,
int height,
Av1InterIntraMode mode,
bool invert)
{
int sizeScale = 128 / Math.Max(width, height);
for (int row = 0; row < height; row++)
{
Span<byte> maskRow = mask.Slice(row * maskStride, width);
for (int column = 0; column < width; column++)
{
int alpha = mode switch
{
Av1InterIntraMode.Vertical => InterIntraWeights[row * sizeScale],
Av1InterIntraMode.Horizontal => InterIntraWeights[column * sizeScale],
Av1InterIntraMode.Smooth => InterIntraWeights[Math.Min(row, column) * sizeScale],
_ => 32,
};
maskRow[column] = (byte)(invert ? MaximumMaskAlpha - alpha : alpha);
}
}
}
/// <summary>
/// Fills an 8-bit difference-weighted compound mask.
/// </summary>
public static void FillDifferenceWeightedMask(
Span<byte> mask,
int maskStride,
ReadOnlySpan<byte> first,
int firstStride,
ReadOnlySpan<byte> second,
int secondStride,
int width,
int height,
Av1DifferenceWeightedMaskType maskType)
{
bool invert = maskType == Av1DifferenceWeightedMaskType.Type38Inverse;
for (int row = 0; row < height; row++)
{
Span<byte> maskRow = mask.Slice(row * maskStride, width);
ReadOnlySpan<byte> firstRow = first.Slice(row * firstStride, width);
ReadOnlySpan<byte> secondRow = second.Slice(row * secondStride, width);
ref byte maskReference = ref MemoryMarshal.GetReference(maskRow);
ref byte firstReference = ref MemoryMarshal.GetReference(firstRow);
ref byte secondReference = ref MemoryMarshal.GetReference(secondRow);
int column = 0;
if (Vector512.IsHardwareAccelerated)
{
int vectorEnd = width - Vector512<byte>.Count;
for (; column <= vectorEnd; column += Vector512<byte>.Count)
{
Vector512<byte> firstVector = Vector512.LoadUnsafe(ref firstReference, (nuint)column);
Vector512<byte> secondVector = Vector512.LoadUnsafe(ref secondReference, (nuint)column);
DifferenceWeighted(firstVector, secondVector, 4, invert).StoreUnsafe(ref maskReference, (nuint)column);
}
}
if (Vector256.IsHardwareAccelerated)
{
int vectorEnd = width - Vector256<byte>.Count;
for (; column <= vectorEnd; column += Vector256<byte>.Count)
{
Vector256<byte> firstVector = Vector256.LoadUnsafe(ref firstReference, (nuint)column);
Vector256<byte> secondVector = Vector256.LoadUnsafe(ref secondReference, (nuint)column);
DifferenceWeighted(firstVector, secondVector, 4, invert).StoreUnsafe(ref maskReference, (nuint)column);
}
}
if (Vector128.IsHardwareAccelerated)
{
int vectorEnd = width - Vector128<byte>.Count;
for (; column <= vectorEnd; column += Vector128<byte>.Count)
{
Vector128<byte> firstVector = Vector128.LoadUnsafe(ref firstReference, (nuint)column);
Vector128<byte> secondVector = Vector128.LoadUnsafe(ref secondReference, (nuint)column);
DifferenceWeighted(firstVector, secondVector, 4, invert).StoreUnsafe(ref maskReference, (nuint)column);
}
}
for (; column < width; column++)
{
int difference = Math.Abs(firstRow[column] - secondRow[column]) >> 4;
int alpha = Math.Min(MaximumMaskAlpha, 38 + difference);
maskRow[column] = (byte)(invert ? MaximumMaskAlpha - alpha : alpha);
}
}
}
/// <summary>
/// Fills a high-bit-depth difference-weighted compound mask.
/// </summary>
public static void FillDifferenceWeightedMask(
Span<byte> mask,
int maskStride,
ReadOnlySpan<ushort> first,
int firstStride,
ReadOnlySpan<ushort> second,
int secondStride,
int width,
int height,
int bitDepth,
Av1DifferenceWeightedMaskType maskType)
{
bool invert = maskType == Av1DifferenceWeightedMaskType.Type38Inverse;
int differenceShift = bitDepth - 8 + 4;
for (int row = 0; row < height; row++)
{
Span<byte> maskRow = mask.Slice(row * maskStride, width);
ReadOnlySpan<ushort> firstRow = first.Slice(row * firstStride, width);
ReadOnlySpan<ushort> secondRow = second.Slice(row * secondStride, width);
ref byte maskReference = ref MemoryMarshal.GetReference(maskRow);
ref ushort firstReference = ref MemoryMarshal.GetReference(firstRow);
ref ushort secondReference = ref MemoryMarshal.GetReference(secondRow);
int column = 0;
// Two input vectors narrow to one packed byte mask. This keeps mask construction contiguous and avoids
// temporary buffers before the following vector blend consumes the complete plane block.
if (Vector512.IsHardwareAccelerated)
{
int vectorEnd = width - Vector512<byte>.Count;
for (; column <= vectorEnd; column += Vector512<byte>.Count)
{
Vector512<ushort> first0 = Vector512.LoadUnsafe(ref firstReference, (nuint)column);
Vector512<ushort> first1 = Vector512.LoadUnsafe(ref firstReference, (nuint)(column + Vector512<ushort>.Count));
Vector512<ushort> second0 = Vector512.LoadUnsafe(ref secondReference, (nuint)column);
Vector512<ushort> second1 = Vector512.LoadUnsafe(ref secondReference, (nuint)(column + Vector512<ushort>.Count));
DifferenceWeighted(first0, first1, second0, second1, differenceShift, invert)
.StoreUnsafe(ref maskReference, (nuint)column);
}
}
if (Vector256.IsHardwareAccelerated)
{
int vectorEnd = width - Vector256<byte>.Count;
for (; column <= vectorEnd; column += Vector256<byte>.Count)
{
Vector256<ushort> first0 = Vector256.LoadUnsafe(ref firstReference, (nuint)column);
Vector256<ushort> first1 = Vector256.LoadUnsafe(ref firstReference, (nuint)(column + Vector256<ushort>.Count));
Vector256<ushort> second0 = Vector256.LoadUnsafe(ref secondReference, (nuint)column);
Vector256<ushort> second1 = Vector256.LoadUnsafe(ref secondReference, (nuint)(column + Vector256<ushort>.Count));
DifferenceWeighted(first0, first1, second0, second1, differenceShift, invert)
.StoreUnsafe(ref maskReference, (nuint)column);
}
}
if (Vector128.IsHardwareAccelerated)
{
int vectorEnd = width - Vector128<byte>.Count;
for (; column <= vectorEnd; column += Vector128<byte>.Count)
{
Vector128<ushort> first0 = Vector128.LoadUnsafe(ref firstReference, (nuint)column);
Vector128<ushort> first1 = Vector128.LoadUnsafe(ref firstReference, (nuint)(column + Vector128<ushort>.Count));
Vector128<ushort> second0 = Vector128.LoadUnsafe(ref secondReference, (nuint)column);
Vector128<ushort> second1 = Vector128.LoadUnsafe(ref secondReference, (nuint)(column + Vector128<ushort>.Count));
DifferenceWeighted(first0, first1, second0, second1, differenceShift, invert)
.StoreUnsafe(ref maskReference, (nuint)column);
}
}
for (; column < width; column++)
{
int difference = Math.Abs(firstRow[column] - secondRow[column]) >> differenceShift;
int alpha = Math.Min(MaximumMaskAlpha, 38 + difference);
maskRow[column] = (byte)(invert ? MaximumMaskAlpha - alpha : alpha);
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> DifferenceWeighted(Vector128<byte> first, Vector128<byte> second, int shift, bool invert)
{
Vector128<byte> difference = Vector128.Max(first, second) - Vector128.Min(first, second);
Vector128<ushort> lower = DifferenceWeightedAlpha(Vector128.WidenLower(difference), shift, invert);
Vector128<ushort> upper = DifferenceWeightedAlpha(Vector128.WidenUpper(difference), shift, invert);
return Vector128.Narrow(lower, upper);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<byte> DifferenceWeighted(Vector256<byte> first, Vector256<byte> second, int shift, bool invert)
{
Vector256<byte> difference = Vector256.Max(first, second) - Vector256.Min(first, second);
Vector256<ushort> lower = DifferenceWeightedAlpha(Vector256.WidenLower(difference), shift, invert);
Vector256<ushort> upper = DifferenceWeightedAlpha(Vector256.WidenUpper(difference), shift, invert);
return Vector256.Narrow(lower, upper);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<byte> DifferenceWeighted(Vector512<byte> first, Vector512<byte> second, int shift, bool invert)
{
Vector512<byte> difference = Vector512.Max(first, second) - Vector512.Min(first, second);
Vector512<ushort> lower = DifferenceWeightedAlpha(Vector512.WidenLower(difference), shift, invert);
Vector512<ushort> upper = DifferenceWeightedAlpha(Vector512.WidenUpper(difference), shift, invert);
return Vector512.Narrow(lower, upper);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> DifferenceWeighted(
Vector128<ushort> first0,
Vector128<ushort> first1,
Vector128<ushort> second0,
Vector128<ushort> second1,
int shift,
bool invert)
=> Vector128.Narrow(
DifferenceWeightedAlpha(Vector128.Max(first0, second0) - Vector128.Min(first0, second0), shift, invert),
DifferenceWeightedAlpha(Vector128.Max(first1, second1) - Vector128.Min(first1, second1), shift, invert));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<byte> DifferenceWeighted(
Vector256<ushort> first0,
Vector256<ushort> first1,
Vector256<ushort> second0,
Vector256<ushort> second1,
int shift,
bool invert)
=> Vector256.Narrow(
DifferenceWeightedAlpha(Vector256.Max(first0, second0) - Vector256.Min(first0, second0), shift, invert),
DifferenceWeightedAlpha(Vector256.Max(first1, second1) - Vector256.Min(first1, second1), shift, invert));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<byte> DifferenceWeighted(
Vector512<ushort> first0,
Vector512<ushort> first1,
Vector512<ushort> second0,
Vector512<ushort> second1,
int shift,
bool invert)
=> Vector512.Narrow(
DifferenceWeightedAlpha(Vector512.Max(first0, second0) - Vector512.Min(first0, second0), shift, invert),
DifferenceWeightedAlpha(Vector512.Max(first1, second1) - Vector512.Min(first1, second1), shift, invert));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<ushort> DifferenceWeightedAlpha(Vector128<ushort> difference, int shift, bool invert)
{
Vector128<ushort> maximum = Vector128.Create((ushort)MaximumMaskAlpha);
Vector128<ushort> alpha = Vector128.Min(maximum, (difference >> shift) + Vector128.Create((ushort)38));
return invert ? maximum - alpha : alpha;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<ushort> DifferenceWeightedAlpha(Vector256<ushort> difference, int shift, bool invert)
{
Vector256<ushort> maximum = Vector256.Create((ushort)MaximumMaskAlpha);
Vector256<ushort> alpha = Vector256.Min(maximum, (difference >> shift) + Vector256.Create((ushort)38));
return invert ? maximum - alpha : alpha;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<ushort> DifferenceWeightedAlpha(Vector512<ushort> difference, int shift, bool invert)
{
Vector512<ushort> maximum = Vector512.Create((ushort)MaximumMaskAlpha);
Vector512<ushort> alpha = Vector512.Min(maximum, (difference >> shift) + Vector512.Create((ushort)38));
return invert ? maximum - alpha : alpha;
}
}

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
/// <summary>
/// Combines two AV1 inter predictors with equal-weight rounded averaging.
/// </summary>
internal static class Av1CompoundInterPredictor
internal static partial class Av1CompoundInterPredictor
{
/// <summary>
/// Averages an 8-bit predictor into an existing prediction block.

204
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WedgeMask.cs

@ -0,0 +1,204 @@
// 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.Inter;
/// <summary>
/// Produces AV1 wedge masks in caller-owned plane-sized storage.
/// </summary>
internal static class Av1WedgeMask
{
private const int MaximumAlpha = 64;
private const int MasterSize = 64;
/// <summary>
/// Gets the odd-row oblique prototype from pinned libaom.
/// </summary>
private static ReadOnlySpan<byte> MasterObliqueOdd =>
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 6, 18,
37, 53, 60, 63, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
];
/// <summary>
/// Gets the even-row oblique prototype from pinned libaom.
/// </summary>
private static ReadOnlySpan<byte> MasterObliqueEven =>
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 4, 11, 27,
46, 58, 62, 63, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
];
/// <summary>
/// Gets the vertical prototype from pinned libaom.
/// </summary>
private static ReadOnlySpan<byte> MasterVertical =>
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 7, 21,
43, 57, 62, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
];
/// <summary>
/// Gets the codebook used when block height exceeds block width.
/// </summary>
private static ReadOnlySpan<byte> HeightGreaterCodebook =>
[
2, 4, 4, 3, 4, 4, 4, 4, 4, 5, 4, 4,
0, 4, 2, 0, 4, 4, 0, 4, 6, 1, 4, 4,
2, 4, 2, 2, 4, 6, 5, 4, 2, 5, 4, 6,
3, 2, 4, 3, 6, 4, 4, 2, 4, 4, 6, 4,
];
/// <summary>
/// Gets the codebook used when block width exceeds block height.
/// </summary>
private static ReadOnlySpan<byte> HeightLessCodebook =>
[
2, 4, 4, 3, 4, 4, 4, 4, 4, 5, 4, 4,
1, 2, 4, 1, 4, 4, 1, 6, 4, 0, 4, 4,
2, 4, 2, 2, 4, 6, 5, 4, 2, 5, 4, 6,
3, 2, 4, 3, 6, 4, 4, 2, 4, 4, 6, 4,
];
/// <summary>
/// Gets the codebook used by square blocks.
/// </summary>
private static ReadOnlySpan<byte> EqualCodebook =>
[
2, 4, 4, 3, 4, 4, 4, 4, 4, 5, 4, 4,
0, 4, 2, 0, 4, 6, 1, 2, 4, 1, 6, 4,
2, 4, 2, 2, 4, 6, 5, 4, 2, 5, 4, 6,
3, 2, 4, 3, 6, 4, 4, 2, 4, 4, 6, 4,
];
/// <summary>
/// Fills one luma or subsampled chroma mask for a selected wedge.
/// </summary>
/// <param name="destination">The caller-owned plane mask.</param>
/// <param name="destinationStride">The distance between destination rows.</param>
/// <param name="blockSize">The luma block size selecting the wedge codebook.</param>
/// <param name="wedgeIndex">The wedge index in the inclusive range zero through fifteen.</param>
/// <param name="wedgeSign">The signaled compound wedge orientation.</param>
/// <param name="subX">The horizontal plane subsampling shift.</param>
/// <param name="subY">The vertical plane subsampling shift.</param>
/// <param name="invert">Whether to complement the resulting mask.</param>
public static void Fill(
Span<byte> destination,
int destinationStride,
Av1BlockSize blockSize,
int wedgeIndex,
bool wedgeSign,
int subX,
int subY,
bool invert)
{
int lumaWidth = blockSize.GetWidth();
int lumaHeight = blockSize.GetHeight();
int width = Math.Max(4, lumaWidth >> subX);
int height = Math.Max(4, lumaHeight >> subY);
ReadOnlySpan<byte> codebook = lumaHeight > lumaWidth
? HeightGreaterCodebook
: lumaHeight < lumaWidth ? HeightLessCodebook : EqualCodebook;
int codebookOffset = wedgeIndex * 3;
int direction = codebook[codebookOffset];
int horizontalOffset = (codebook[codebookOffset + 1] * lumaWidth) >> 3;
int verticalOffset = (codebook[codebookOffset + 2] * lumaHeight) >> 3;
bool negative = wedgeSign ^ GetSignFlip(blockSize, wedgeIndex);
int masterRow = (MasterSize / 2) - verticalOffset;
int masterColumn = (MasterSize / 2) - horizontalOffset;
// Chroma masks are the rounded average of the corresponding two or four luma-mask samples. Producing the
// plane mask once keeps the vector blend contiguous and avoids gathering mask bytes in every SIMD lane.
for (int row = 0; row < height; row++)
{
Span<byte> destinationRow = destination.Slice(row * destinationStride, width);
int lumaRow = row << subY;
for (int column = 0; column < width; column++)
{
int lumaColumn = column << subX;
int mask = GetMasterValue(direction, negative, masterRow + lumaRow, masterColumn + lumaColumn);
if (subX != 0)
{
mask += GetMasterValue(direction, negative, masterRow + lumaRow, masterColumn + lumaColumn + 1);
}
if (subY != 0)
{
int lowerMask = GetMasterValue(direction, negative, masterRow + lumaRow + 1, masterColumn + lumaColumn);
if (subX != 0)
{
lowerMask += GetMasterValue(direction, negative, masterRow + lumaRow + 1, masterColumn + lumaColumn + 1);
}
mask += lowerMask;
}
int sampleCountShift = subX + subY;
if (sampleCountShift != 0)
{
mask = (mask + (1 << (sampleCountShift - 1))) >> sampleCountShift;
}
destinationRow[column] = (byte)(invert ? MaximumAlpha - mask : mask);
}
}
}
/// <summary>
/// Reads one value from the generated 64 by 64 master mask.
/// </summary>
private static int GetMasterValue(int direction, bool negative, int row, int column)
{
int value = direction switch
{
0 => MasterVertical[row],
1 => MasterVertical[column],
2 => GetOblique63(column, row),
3 => GetOblique63(row, column),
4 => MaximumAlpha - GetOblique63(row, MasterSize - 1 - column),
_ => MaximumAlpha - GetOblique63(column, MasterSize - 1 - row),
};
return negative ? MaximumAlpha - value : value;
}
/// <summary>
/// Reads one value from the shifted oblique-63 master prototype.
/// </summary>
private static int GetOblique63(int row, int column)
{
bool oddRow = (row & 1) != 0;
int shift = (oddRow ? 15 : 16) - (row >> 1);
int sourceColumn = Av1Math.Clip3(0, MasterSize - 1, column - shift);
return oddRow ? MasterObliqueOdd[sourceColumn] : MasterObliqueEven[sourceColumn];
}
/// <summary>
/// Gets libaom's canonical sign flip for a block and wedge index.
/// </summary>
private static bool GetSignFlip(Av1BlockSize blockSize, int wedgeIndex)
{
ReadOnlySpan<byte> signFlips = blockSize switch
{
Av1BlockSize.Block8x8 or Av1BlockSize.Block16x16 or Av1BlockSize.Block32x32 =>
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1],
Av1BlockSize.Block8x32 =>
[1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1],
Av1BlockSize.Block32x8 =>
[1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1],
_ =>
[1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1],
};
return signFlips[wedgeIndex] != 0;
}
}

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

@ -2019,6 +2019,8 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
}
modeInfo.MotionMode = Av1MotionMode.SimpleTranslation;
modeInfo.UseInterIntraWedge = false;
modeInfo.InterIntraWedgeIndex = 0;
int minimumBlockDimension = Math.Min(modeInfo.BlockSize.GetWidth(), modeInfo.BlockSize.GetHeight());
if (!isCompound && !modeInfo.SkipMode &&
@ -2026,13 +2028,25 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
modeInfo.BlockSize is >= Av1BlockSize.Block8x8 and <= Av1BlockSize.Block32x32 &&
reader.ReadIsInterIntra(modeInfo.BlockSize))
{
// AV1 assigns this syntax only to the contiguous Block8x8 through Block32x32 enum range; similarly
// dimensioned extended rectangles occur later in the enum and must not consume a flag. A false flag
// continues into motion-mode syntax even while the selected inter-intra predictor remains unsupported.
throw new NotSupportedException("AV1 inter-intra block prediction is not implemented.");
// The synthetic INTRA_FRAME second reference is part of the decoded mode state: it suppresses motion
// variation syntax and lets reconstruction distinguish inter-intra from a regular single-reference block.
modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Intra;
modeInfo.InterIntraMode = reader.ReadInterIntraMode(modeInfo.BlockSize);
modeInfo.SetAngleDelta(Av1PlaneType.Y, 0);
modeInfo.SetAngleDelta(Av1PlaneType.Uv, 0);
modeInfo.UseFilterIntra = false;
modeInfo.UseInterIntraWedge = reader.ReadUseInterIntraWedge(modeInfo.BlockSize);
if (modeInfo.UseInterIntraWedge)
{
modeInfo.InterIntraWedgeIndex = reader.ReadWedgeIndex(modeInfo.BlockSize);
}
}
if (!isCompound && this.FrameHeader.IsMotionModeSwitchable && minimumBlockDimension >= 8 && !modeInfo.SkipMode)
if (!isCompound &&
modeInfo.ReferenceFrames[1] != Av1ReferenceFrameType.Intra &&
this.FrameHeader.IsMotionModeSwitchable &&
minimumBlockDimension >= 8 &&
!modeInfo.SkipMode)
{
Av1MotionVariationCandidates candidates = this.motionVariationCandidates;
candidates.Build(ref partitionInfo, tileInfo, this.SequenceHeader, this.FrameHeader, referenceFrame);
@ -2070,12 +2084,65 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
modeInfo.CompoundGroupIndex = false;
modeInfo.CompoundIndex = true;
modeInfo.CompoundType = Av1CompoundType.Average;
if (isCompound && !modeInfo.SkipMode &&
(this.SequenceHeader.EnableMaskedCompound || this.SequenceHeader.OrderHintInfo.EnableJointCompound))
modeInfo.CompoundWedgeIndex = 0;
modeInfo.CompoundWedgeSign = false;
modeInfo.DifferenceWeightedMaskType = Av1DifferenceWeightedMaskType.Type38;
if (isCompound && !modeInfo.SkipMode)
{
// Either enabled sequence tool adds a compound-selection symbol before interpolation syntax. Refuse
// that later checkpoint at its owning boundary so this equal-average path cannot desynchronize tiles.
throw new NotSupportedException("AV1 selectable compound blending is not implemented.");
bool maskedCompoundUsed = minimumBlockDimension >= 8 && this.SequenceHeader.EnableMaskedCompound;
if (maskedCompoundUsed)
{
int groupContext = Av1SymbolContextHelper.GetCompoundGroupIndexContext(
partitionInfo.AboveModeInfo,
partitionInfo.LeftModeInfo);
modeInfo.CompoundGroupIndex = reader.ReadCompoundGroupIndex(groupContext);
}
if (!modeInfo.CompoundGroupIndex)
{
if (this.SequenceHeader.OrderHintInfo.EnableJointCompound)
{
int compoundIndexContext = Av1SymbolContextHelper.GetCompoundIndexContext(
this.SequenceHeader.OrderHintInfo,
this.FrameHeader,
modeInfo,
partitionInfo.AboveModeInfo,
partitionInfo.LeftModeInfo);
modeInfo.CompoundIndex = reader.ReadCompoundIndex(compoundIndexContext);
modeInfo.CompoundType = modeInfo.CompoundIndex
? Av1CompoundType.Average
: Av1CompoundType.DistanceWeighted;
}
}
else
{
bool supportsWedge = modeInfo.BlockSize is
Av1BlockSize.Block8x8 or
Av1BlockSize.Block8x16 or
Av1BlockSize.Block16x8 or
Av1BlockSize.Block16x16 or
Av1BlockSize.Block16x32 or
Av1BlockSize.Block32x16 or
Av1BlockSize.Block32x32 or
Av1BlockSize.Block8x32 or
Av1BlockSize.Block32x8;
modeInfo.CompoundType = supportsWedge
? reader.ReadMaskedCompoundType(modeInfo.BlockSize)
: Av1CompoundType.DifferenceWeighted;
if (modeInfo.CompoundType == Av1CompoundType.Wedge)
{
modeInfo.CompoundWedgeIndex = reader.ReadWedgeIndex(modeInfo.BlockSize);
modeInfo.CompoundWedgeSign = reader.ReadLiteral(1) != 0;
}
else
{
modeInfo.DifferenceWeightedMaskType = (Av1DifferenceWeightedMaskType)reader.ReadLiteral(1);
}
}
}
Span<Av1InterpolationFilter> interpolationFilters = modeInfo.InterpolationFilters;

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

@ -139,7 +139,10 @@ internal sealed class Av1BlockDecoder : IDisposable
this.inverseQuantizationOwner = inverseQuantizationOwner;
this.transformWorkspaceOwner = transformWorkspaceOwner;
this.predictionScratchOwner = predictionScratchOwner;
this.predictionDecoder = new(sequenceHeader, frameHeader, predictionScratchOwner.Memory);
this.predictionDecoder = new(
sequenceHeader,
frameHeader,
predictionScratchOwner.Memory[maximumBlockArea..]);
this.isLoopFilterEnabled = frameHeader.LoopFilterParameters.FilterLevel[0] != 0 ||
frameHeader.LoopFilterParameters.FilterLevel[1] != 0;
@ -241,6 +244,9 @@ internal sealed class Av1BlockDecoder : IDisposable
Av1FrameBuffer<byte>? referenceFrameBuffer = null;
Av1FrameBuffer<byte>? secondaryReferenceFrameBuffer = null;
bool isCompound = modeInfo.ReferenceFrames[1] > Av1ReferenceFrameType.Intra;
bool isInterIntra = modeInfo.ReferenceFrames[1] == Av1ReferenceFrameType.Intra;
int firstCompoundWeight = 8;
int secondCompoundWeight = 8;
if (isInterBlock)
{
int canonicalReferenceIndex = (int)modeInfo.ReferenceFrames[0] - (int)Av1ReferenceFrameType.Last;
@ -270,12 +276,17 @@ internal sealed class Av1BlockDecoder : IDisposable
throw new NotSupportedException("AV1 non-translational global prediction is not implemented.");
}
if (modeInfo.CompoundType != Av1CompoundType.Average)
secondaryReferenceFrameBuffer = this.ResolveReferenceFrame(modeInfo.ReferenceFrames[1]);
if (modeInfo.CompoundType == Av1CompoundType.DistanceWeighted)
{
throw new NotSupportedException($"AV1 {modeInfo.CompoundType} compound prediction is not implemented.");
Av1CompoundDistanceWeights.Derive(
this.sequenceHeader.OrderHintInfo,
this.frameHeader,
modeInfo.ReferenceFrames[0],
modeInfo.ReferenceFrames[1],
out firstCompoundWeight,
out secondCompoundWeight);
}
secondaryReferenceFrameBuffer = this.ResolveReferenceFrame(modeInfo.ReferenceFrames[1]);
}
}
@ -343,6 +354,7 @@ internal sealed class Av1BlockDecoder : IDisposable
Span<short> predictionScratch = predictionStorage[maximumBlockArea..];
Span<byte> secondPrediction = MemoryMarshal.AsBytes(secondPredictionStorage)[..(predictionWidth * predictionHeight)];
Span<ushort> highBitDepthSecondPrediction = MemoryMarshal.Cast<short, ushort>(secondPredictionStorage)[..(predictionWidth * predictionHeight)];
Span<byte> compoundMask = MemoryMarshal.AsBytes(predictionScratch)[..(predictionWidth * predictionHeight)];
int referenceCount = isCompound ? 2 : 1;
for (int referenceIndex = 0; referenceIndex < referenceCount; referenceIndex++)
@ -455,21 +467,242 @@ internal sealed class Av1BlockDecoder : IDisposable
Span<ushort> destination = MemoryMarshal.Cast<short, ushort>(
highBitDepthBlockReconstructionBuffer[reconstructionStride..]);
Av1CompoundInterPredictor.Average(
destination,
switch (modeInfo.CompoundType)
{
case Av1CompoundType.Average:
Av1CompoundInterPredictor.Average(
destination,
reconstructionStride,
highBitDepthSecondPrediction,
predictionWidth,
predictionWidth,
predictionHeight);
break;
case Av1CompoundType.DistanceWeighted:
Av1CompoundInterPredictor.DistanceWeighted(
destination,
reconstructionStride,
highBitDepthSecondPrediction,
predictionWidth,
predictionWidth,
predictionHeight,
firstCompoundWeight,
secondCompoundWeight);
break;
case Av1CompoundType.Wedge:
Av1WedgeMask.Fill(
compoundMask,
predictionWidth,
blockSize,
modeInfo.CompoundWedgeIndex,
modeInfo.CompoundWedgeSign,
subX,
subY,
invert: false);
Av1CompoundInterPredictor.Blend(
destination,
reconstructionStride,
highBitDepthSecondPrediction,
predictionWidth,
compoundMask,
predictionWidth,
predictionWidth,
predictionHeight);
break;
default:
Av1CompoundInterPredictor.FillDifferenceWeightedMask(
compoundMask,
predictionWidth,
destination,
reconstructionStride,
highBitDepthSecondPrediction,
predictionWidth,
predictionWidth,
predictionHeight,
this.frameBuffer.BitDepth.GetBitCount(),
modeInfo.DifferenceWeightedMaskType);
Av1CompoundInterPredictor.Blend(
destination,
reconstructionStride,
highBitDepthSecondPrediction,
predictionWidth,
compoundMask,
predictionWidth,
predictionWidth,
predictionHeight);
break;
}
}
else
{
Span<byte> destination = blockReconstructionBuffer[reconstructionStride..];
switch (modeInfo.CompoundType)
{
case Av1CompoundType.Average:
Av1CompoundInterPredictor.Average(
destination,
reconstructionStride,
secondPrediction,
predictionWidth,
predictionWidth,
predictionHeight);
break;
case Av1CompoundType.DistanceWeighted:
Av1CompoundInterPredictor.DistanceWeighted(
destination,
reconstructionStride,
secondPrediction,
predictionWidth,
predictionWidth,
predictionHeight,
firstCompoundWeight,
secondCompoundWeight);
break;
case Av1CompoundType.Wedge:
Av1WedgeMask.Fill(
compoundMask,
predictionWidth,
blockSize,
modeInfo.CompoundWedgeIndex,
modeInfo.CompoundWedgeSign,
subX,
subY,
invert: false);
Av1CompoundInterPredictor.Blend(
destination,
reconstructionStride,
secondPrediction,
predictionWidth,
compoundMask,
predictionWidth,
predictionWidth,
predictionHeight);
break;
default:
Av1CompoundInterPredictor.FillDifferenceWeightedMask(
compoundMask,
predictionWidth,
destination,
reconstructionStride,
secondPrediction,
predictionWidth,
predictionWidth,
predictionHeight,
modeInfo.DifferenceWeightedMaskType);
Av1CompoundInterPredictor.Blend(
destination,
reconstructionStride,
secondPrediction,
predictionWidth,
compoundMask,
predictionWidth,
predictionWidth,
predictionHeight);
break;
}
}
}
else if (isInterIntra)
{
if (highBitDepth)
{
this.predictionDecoder.DecodeInterIntra(
ref partitionInfo,
(Av1Plane)plane,
tileInfo,
highBitDepthBlockReconstructionBuffer,
reconstructionStride,
secondPredictionStorage[..(predictionWidth * predictionHeight)],
predictionWidth,
this.frameBuffer.BitDepth);
if (modeInfo.UseInterIntraWedge)
{
Av1WedgeMask.Fill(
compoundMask,
predictionWidth,
blockSize,
modeInfo.InterIntraWedgeIndex,
wedgeSign: false,
subX,
subY,
invert: true);
}
else
{
Av1CompoundInterPredictor.FillInterIntraMask(
compoundMask,
predictionWidth,
predictionWidth,
predictionHeight,
modeInfo.InterIntraMode,
invert: true);
}
Av1CompoundInterPredictor.Blend(
MemoryMarshal.Cast<short, ushort>(highBitDepthBlockReconstructionBuffer[reconstructionStride..]),
reconstructionStride,
highBitDepthSecondPrediction,
predictionWidth,
compoundMask,
predictionWidth,
predictionWidth,
predictionHeight);
}
else
{
Av1CompoundInterPredictor.Average(
this.predictionDecoder.DecodeInterIntra(
ref partitionInfo,
(Av1Plane)plane,
tileInfo,
blockReconstructionBuffer,
reconstructionStride,
secondPrediction,
predictionWidth,
this.frameBuffer.BitDepth);
if (modeInfo.UseInterIntraWedge)
{
Av1WedgeMask.Fill(
compoundMask,
predictionWidth,
blockSize,
modeInfo.InterIntraWedgeIndex,
wedgeSign: false,
subX,
subY,
invert: true);
}
else
{
Av1CompoundInterPredictor.FillInterIntraMask(
compoundMask,
predictionWidth,
predictionWidth,
predictionHeight,
modeInfo.InterIntraMode,
invert: true);
}
Av1CompoundInterPredictor.Blend(
blockReconstructionBuffer[reconstructionStride..],
reconstructionStride,
secondPrediction,
predictionWidth,
compoundMask,
predictionWidth,
predictionWidth,
predictionHeight);
}

208
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs

@ -110,6 +110,214 @@ public class Av1CompoundBlockDecoderTests
}
}
/// <summary>
/// Verifies selectable compound reconstruction through the production block branch at every supported bit depth.
/// </summary>
/// <param name="bitDepthValue">The native sample depth.</param>
/// <param name="compoundTypeValue">The selected compound operation.</param>
[Theory]
[InlineData((int)Av1BitDepth.EightBit, (int)Av1CompoundType.DistanceWeighted)]
[InlineData((int)Av1BitDepth.TenBit, (int)Av1CompoundType.DistanceWeighted)]
[InlineData((int)Av1BitDepth.TwelveBit, (int)Av1CompoundType.DistanceWeighted)]
[InlineData((int)Av1BitDepth.EightBit, (int)Av1CompoundType.Wedge)]
[InlineData((int)Av1BitDepth.TenBit, (int)Av1CompoundType.Wedge)]
[InlineData((int)Av1BitDepth.TwelveBit, (int)Av1CompoundType.Wedge)]
[InlineData((int)Av1BitDepth.EightBit, (int)Av1CompoundType.DifferenceWeighted)]
[InlineData((int)Av1BitDepth.TenBit, (int)Av1CompoundType.DifferenceWeighted)]
[InlineData((int)Av1BitDepth.TwelveBit, (int)Av1CompoundType.DifferenceWeighted)]
public void DecodeBlockReconstructsSelectableCompoundPrediction(int bitDepthValue, int compoundTypeValue)
{
Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue;
Av1CompoundType compoundType = (Av1CompoundType)compoundTypeValue;
ushort firstValue = bitDepth == Av1BitDepth.EightBit ? (ushort)20 : (ushort)100;
ushort secondValue = bitDepth switch
{
Av1BitDepth.EightBit => 41,
Av1BitDepth.TenBit => 701,
_ => 3001,
};
ReadOnlySpan<byte> wedgeMask =
[
0, 0, 0, 1, 1, 2, 4, 6,
0, 1, 1, 2, 4, 6, 11, 18,
1, 2, 4, 6, 11, 18, 27, 37,
4, 6, 11, 18, 27, 37, 46, 53,
11, 18, 27, 37, 46, 53, 58, 60,
27, 37, 46, 53, 58, 60, 62, 63,
46, 53, 58, 60, 62, 63, 63, 64,
58, 60, 62, 63, 63, 64, 64, 64,
];
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth);
sequenceHeader.OrderHintInfo.EnableOrderHint = true;
sequenceHeader.OrderHintInfo.OrderHintBits = 5;
ObuFrameHeader frameHeader = CreateFrameHeader();
frameHeader.OrderHint = 10;
frameHeader.GetReferenceFrameIndices()[0] = 0;
frameHeader.GetReferenceFrameIndices()[1] = 1;
frameHeader.GetReferenceOrderHints()[0] = 9;
frameHeader.GetReferenceOrderHints()[1] = 5;
using Av1ReferenceFrameStore referenceFrames = new();
Assert.True(referenceFrames.Commit(1, CreateReferenceFrame(sequenceHeader, firstValue), showFrame: false));
Assert.True(referenceFrames.Commit(2, CreateReferenceFrame(sequenceHeader, secondValue), showFrame: false));
using Av1FrameBuffer<byte> frameBuffer = new(
Configuration.Default,
sequenceHeader,
Av1ColorFormat.Yuv400,
false);
using Av1FrameInfo frameInfo = new(sequenceHeader);
Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty);
superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0);
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty)
{
Skip = true,
YMode = Av1PredictionMode.NearestNearestMotionVector,
CompoundIndex = compoundType != Av1CompoundType.DistanceWeighted,
CompoundType = compoundType,
CompoundWedgeIndex = 0,
CompoundWedgeSign = true,
DifferenceWeightedMaskType = Av1DifferenceWeightedMaskType.Type38,
};
modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last;
modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Last2;
modeInfo.InterpolationFilters.Clear();
modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1);
Av1LoopFilterContext loopFilterContext = new(sequenceHeader);
Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader);
using Av1BlockDecoder decoder = new(
sequenceHeader,
frameHeader,
frameBuffer,
loopFilterContext,
inverseQuantizer,
referenceFrames);
decoder.UpdateSuperblock(superblockInfo);
decoder.DecodeBlock(
modeInfo,
Point.Empty,
Av1BlockSize.Block8x8,
superblockInfo,
new Av1TileInfo(0, 0, frameHeader));
int differenceShift = bitDepth.GetBitCount() - 8 + 4;
int differenceAlpha = Math.Min(64, 38 + (Math.Abs(firstValue - secondValue) >> differenceShift));
for (int row = 0; row < 8; row++)
{
for (int column = 0; column < 8; column++)
{
int alpha = compoundType switch
{
Av1CompoundType.Wedge => wedgeMask[(row * 8) + column],
Av1CompoundType.DifferenceWeighted => differenceAlpha,
_ => 52,
};
ushort expected = (ushort)(((alpha * firstValue) + ((64 - alpha) * secondValue) + 32) >> 6);
if (bitDepth == Av1BitDepth.EightBit)
{
Span<byte> samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row);
Assert.Equal((byte)expected, samples[column]);
}
else
{
Span<ushort> samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0);
Assert.Equal(expected, samples[column]);
}
}
}
}
/// <summary>
/// Verifies smooth inter-intra reconstruction through the production block branch at every supported bit depth.
/// </summary>
/// <param name="bitDepthValue">The native sample depth.</param>
[Theory]
[InlineData((int)Av1BitDepth.EightBit)]
[InlineData((int)Av1BitDepth.TenBit)]
[InlineData((int)Av1BitDepth.TwelveBit)]
public void DecodeBlockReconstructsSmoothInterIntraPrediction(int bitDepthValue)
{
Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue;
ushort interValue = bitDepth == Av1BitDepth.EightBit ? (ushort)20 : (ushort)100;
ushort intraValue = (ushort)(1 << (bitDepth.GetBitCount() - 1));
ushort expected = (ushort)((interValue + intraValue + 1) >> 1);
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth);
ObuFrameHeader frameHeader = CreateFrameHeader();
frameHeader.GetReferenceFrameIndices()[0] = 0;
using Av1ReferenceFrameStore referenceFrames = new();
Assert.True(referenceFrames.Commit(1, CreateReferenceFrame(sequenceHeader, interValue), showFrame: false));
using Av1FrameBuffer<byte> frameBuffer = new(
Configuration.Default,
sequenceHeader,
Av1ColorFormat.Yuv400,
false);
using Av1FrameInfo frameInfo = new(sequenceHeader);
Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty);
superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0);
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty)
{
Skip = true,
YMode = Av1PredictionMode.NearestMotionVector,
InterIntraMode = Av1InterIntraMode.DC,
UseInterIntraWedge = false,
};
modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last;
modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Intra;
modeInfo.InterpolationFilters.Clear();
modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1);
Av1LoopFilterContext loopFilterContext = new(sequenceHeader);
Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader);
using Av1BlockDecoder decoder = new(
sequenceHeader,
frameHeader,
frameBuffer,
loopFilterContext,
inverseQuantizer,
referenceFrames);
decoder.UpdateSuperblock(superblockInfo);
decoder.DecodeBlock(
modeInfo,
Point.Empty,
Av1BlockSize.Block8x8,
superblockInfo,
new Av1TileInfo(0, 0, frameHeader));
for (int row = 0; row < 8; row++)
{
if (bitDepth == Av1BitDepth.EightBit)
{
Span<byte> samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row);
for (int column = 0; column < 8; column++)
{
Assert.Equal((byte)expected, samples[column]);
}
}
else
{
Span<ushort> samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0);
for (int column = 0; column < 8; column++)
{
Assert.Equal(expected, samples[column]);
}
}
}
}
/// <summary>
/// Creates one independently owned retained frame filled with a constant visible luma value.
/// </summary>

329
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundInterPredictorTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
@ -32,6 +33,156 @@ public class Av1CompoundInterPredictorTests
public void HighBitDepthAverageMatchesIndependentOracleAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthAverage, PredictorConfigurations);
/// <summary>
/// Verifies 8-bit distance and per-sample mask blending across every intrinsic width and scalar tail.
/// </summary>
[Fact]
public void ByteSelectableBlendsMatchIndependentOracleAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateByteSelectableBlends, PredictorConfigurations);
/// <summary>
/// Verifies 10/12-bit distance and per-sample mask blending across every intrinsic width and scalar tail.
/// </summary>
[Fact]
public void HighBitDepthSelectableBlendsMatchIndependentOracleAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthSelectableBlends, PredictorConfigurations);
/// <summary>
/// Verifies the four smooth inter-intra modes and their complemented destination orientation.
/// </summary>
[Fact]
public void SmoothInterIntraMasksMatchPinnedWeights()
{
ReadOnlySpan<byte> weights = [60, 34, 19, 11, 6, 4, 2, 1];
foreach (Av1InterIntraMode mode in Enum.GetValues<Av1InterIntraMode>())
{
const int width = 8;
const int height = 4;
const int stride = 11;
byte[] mask = new byte[stride * height];
byte[] inverted = new byte[stride * height];
mask.AsSpan().Fill(0xA5);
inverted.AsSpan().Fill(0xA5);
Av1CompoundInterPredictor.FillInterIntraMask(mask, stride, width, height, mode, invert: false);
Av1CompoundInterPredictor.FillInterIntraMask(inverted, stride, width, height, mode, invert: true);
for (int row = 0; row < height; row++)
{
for (int column = 0; column < width; column++)
{
byte expected = mode switch
{
Av1InterIntraMode.Vertical => weights[row],
Av1InterIntraMode.Horizontal => weights[column],
Av1InterIntraMode.Smooth => weights[Math.Min(row, column)],
_ => 32,
};
Assert.Equal(expected, mask[(row * stride) + column]);
Assert.Equal((byte)(64 - expected), inverted[(row * stride) + column]);
}
for (int column = width; column < stride; column++)
{
Assert.Equal(0xA5, mask[(row * stride) + column]);
Assert.Equal(0xA5, inverted[(row * stride) + column]);
}
}
}
}
/// <summary>
/// Verifies both difference-mask orientations at each supported bit depth.
/// </summary>
[Fact]
public void DifferenceWeightedMasksMatchPinnedFormula()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateDifferenceWeightedMasks, PredictorConfigurations);
/// <summary>
/// Applies the pinned difference-mask formula at every bit depth and intrinsic width.
/// </summary>
private static void ValidateDifferenceWeightedMasks()
{
ReadOnlySpan<int> widths = [9, 16, 23, 32, 47, 64, 127];
foreach (int width in widths)
{
const int height = 3;
int firstStride = width + 4;
int secondStride = width + 2;
int maskStride = width + 3;
foreach (int bitDepth in new[] { 8, 10, 12 })
{
int sampleMask = (1 << bitDepth) - 1;
ushort[] first = new ushort[firstStride * height];
ushort[] second = new ushort[secondStride * height];
for (int row = 0; row < height; row++)
{
for (int column = 0; column < width; column++)
{
first[(row * firstStride) + column] = (ushort)(((row * 911) + (column * 521)) & sampleMask);
second[(row * secondStride) + column] = (ushort)(((row * 307) + (column * 997) + 31) & sampleMask);
}
}
foreach (Av1DifferenceWeightedMaskType maskType in Enum.GetValues<Av1DifferenceWeightedMaskType>())
{
byte[] actual = new byte[maskStride * height];
actual.AsSpan().Fill(0xA5);
if (bitDepth == 8)
{
byte[] firstByte = Array.ConvertAll(first, value => (byte)value);
byte[] secondByte = Array.ConvertAll(second, value => (byte)value);
Av1CompoundInterPredictor.FillDifferenceWeightedMask(
actual,
maskStride,
firstByte,
firstStride,
secondByte,
secondStride,
width,
height,
maskType);
}
else
{
Av1CompoundInterPredictor.FillDifferenceWeightedMask(
actual,
maskStride,
first,
firstStride,
second,
secondStride,
width,
height,
bitDepth,
maskType);
}
for (int row = 0; row < height; row++)
{
for (int column = 0; column < width; column++)
{
int difference = Math.Abs(first[(row * firstStride) + column] - second[(row * secondStride) + column]);
int alpha = Math.Min(64, 38 + ((difference >> (bitDepth - 8)) / 16));
byte expected = (byte)(maskType == Av1DifferenceWeightedMaskType.Type38Inverse ? 64 - alpha : alpha);
Assert.Equal(expected, actual[(row * maskStride) + column]);
}
for (int column = width; column < maskStride; column++)
{
Assert.Equal(0xA5, actual[(row * maskStride) + column]);
}
}
}
}
}
}
/// <summary>
/// Applies independent byte arithmetic to block widths that cross every vector and scalar boundary.
/// </summary>
@ -113,6 +264,169 @@ public class Av1CompoundInterPredictorTests
}
}
/// <summary>
/// Applies independent byte arithmetic to every selectable compound blend.
/// </summary>
private static void ValidateByteSelectableBlends()
{
ReadOnlySpan<int> widths = [4, 7, 8, 15, 16, 23, 31, 32, 47, 64, 127, 128];
ReadOnlySpan<int> distanceWeights = [9, 7, 11, 5, 12, 4, 13, 3];
foreach (int width in widths)
{
const int height = 5;
int destinationStride = width + 11;
int secondStride = width + 7;
int maskStride = width + 5;
byte[] first = new byte[destinationStride * height];
byte[] second = new byte[secondStride * height];
byte[] mask = new byte[maskStride * height];
FillByteInputs(first, second, destinationStride, secondStride, width, height);
FillMask(mask, maskStride, width, height);
for (int weightIndex = 0; weightIndex < distanceWeights.Length; weightIndex += 2)
{
byte[] expected = (byte[])first.Clone();
byte[] actual = (byte[])first.Clone();
int firstWeight = distanceWeights[weightIndex];
int secondWeight = distanceWeights[weightIndex + 1];
for (int row = 0; row < height; row++)
{
for (int column = 0; column < width; column++)
{
int destinationIndex = (row * destinationStride) + column;
int secondIndex = (row * secondStride) + column;
expected[destinationIndex] = (byte)(((expected[destinationIndex] * firstWeight) +
(second[secondIndex] * secondWeight) + 8) >> 4);
}
}
Av1CompoundInterPredictor.DistanceWeighted(
actual,
destinationStride,
second,
secondStride,
width,
height,
firstWeight,
secondWeight);
Assert.Equal(expected, actual);
}
byte[] maskedExpected = (byte[])first.Clone();
byte[] maskedActual = (byte[])first.Clone();
for (int row = 0; row < height; row++)
{
for (int column = 0; column < width; column++)
{
int destinationIndex = (row * destinationStride) + column;
int secondIndex = (row * secondStride) + column;
int alpha = mask[(row * maskStride) + column];
maskedExpected[destinationIndex] = (byte)(((alpha * maskedExpected[destinationIndex]) +
((64 - alpha) * second[secondIndex]) + 32) >> 6);
}
}
Av1CompoundInterPredictor.Blend(
maskedActual,
destinationStride,
second,
secondStride,
mask,
maskStride,
width,
height);
Assert.Equal(maskedExpected, maskedActual);
}
}
/// <summary>
/// Applies independent high-bit-depth arithmetic to every selectable compound blend.
/// </summary>
private static void ValidateHighBitDepthSelectableBlends()
{
ReadOnlySpan<int> widths = [4, 7, 8, 15, 16, 23, 31, 32, 47, 64, 127, 128];
ReadOnlySpan<int> distanceWeights = [9, 7, 11, 5, 12, 4, 13, 3];
foreach (int bitDepth in new[] { 10, 12 })
{
foreach (int width in widths)
{
const int height = 5;
int destinationStride = width + 9;
int secondStride = width + 5;
int maskStride = width + 3;
ushort[] first = new ushort[destinationStride * height];
ushort[] second = new ushort[secondStride * height];
byte[] mask = new byte[maskStride * height];
FillHighBitDepthInputs(first, second, destinationStride, secondStride, width, height, bitDepth);
FillMask(mask, maskStride, width, height);
for (int weightIndex = 0; weightIndex < distanceWeights.Length; weightIndex += 2)
{
ushort[] expected = (ushort[])first.Clone();
ushort[] actual = (ushort[])first.Clone();
int firstWeight = distanceWeights[weightIndex];
int secondWeight = distanceWeights[weightIndex + 1];
for (int row = 0; row < height; row++)
{
for (int column = 0; column < width; column++)
{
int destinationIndex = (row * destinationStride) + column;
int secondIndex = (row * secondStride) + column;
expected[destinationIndex] = (ushort)(((expected[destinationIndex] * firstWeight) +
(second[secondIndex] * secondWeight) + 8) >> 4);
}
}
Av1CompoundInterPredictor.DistanceWeighted(
actual,
destinationStride,
second,
secondStride,
width,
height,
firstWeight,
secondWeight);
Assert.Equal(expected, actual);
}
ushort[] maskedExpected = (ushort[])first.Clone();
ushort[] maskedActual = (ushort[])first.Clone();
for (int row = 0; row < height; row++)
{
for (int column = 0; column < width; column++)
{
int destinationIndex = (row * destinationStride) + column;
int secondIndex = (row * secondStride) + column;
int alpha = mask[(row * maskStride) + column];
maskedExpected[destinationIndex] = (ushort)(((alpha * maskedExpected[destinationIndex]) +
((64 - alpha) * second[secondIndex]) + 32) >> 6);
}
}
Av1CompoundInterPredictor.Blend(
maskedActual,
destinationStride,
second,
secondStride,
mask,
maskStride,
width,
height);
Assert.Equal(maskedExpected, maskedActual);
}
}
}
/// <summary>
/// Fills active byte samples while assigning different sentinels to the unused row tails.
/// </summary>
@ -162,4 +476,19 @@ public class Av1CompoundInterPredictorTests
}
}
}
/// <summary>
/// Fills active mask samples across the complete AV1 alpha range while guarding every row tail.
/// </summary>
private static void FillMask(Span<byte> mask, int maskStride, int width, int height)
{
mask.Fill(0xA5);
for (int row = 0; row < height; row++)
{
for (int column = 0; column < width; column++)
{
mask[(row * maskStride) + column] = (byte)(((row * 19) + (column * 37)) % 65);
}
}
}
}

81
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterFrameModeInfoTests.cs

@ -223,6 +223,87 @@ public class Av1InterFrameModeInfoTests
Assert.Equal(Av1CompoundType.Average, modeInfo.CompoundType);
}
/// <summary>
/// Verifies selectable compound syntax in its normative position before interpolation filtering.
/// </summary>
/// <param name="compoundTypeValue">The selected compound operation.</param>
[Theory]
[InlineData((int)Av1CompoundType.DistanceWeighted)]
[InlineData((int)Av1CompoundType.Wedge)]
[InlineData((int)Av1CompoundType.DifferenceWeighted)]
public void ReadInterFrameModeInfoReadsSelectableCompoundBeforeInterpolation(int compoundTypeValue)
{
Av1CompoundType compoundType = (Av1CompoundType)compoundTypeValue;
ObuSequenceHeader sequenceHeader = CreateSequenceHeader();
sequenceHeader.EnableMaskedCompound = true;
sequenceHeader.EnableDualFilter = false;
sequenceHeader.OrderHintInfo.EnableOrderHint = true;
sequenceHeader.OrderHintInfo.EnableJointCompound = true;
sequenceHeader.OrderHintInfo.OrderHintBits = 3;
ObuFrameHeader frameHeader = CreateFrameHeader();
frameHeader.ReferenceMode = ObuReferenceMode.ReferenceModeSelect;
frameHeader.InterpolationFilter = Av1InterpolationFilter.Switchable;
frameHeader.OrderHint = 4;
frameHeader.GetReferenceFrameIndices()[0] = 0;
frameHeader.GetReferenceFrameIndices()[1] = 1;
frameHeader.GetReferenceOrderHints()[0] = 3;
frameHeader.GetReferenceOrderHints()[1] = 5;
using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader);
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty);
using Av1SymbolWriter writer = new(Configuration.Default, 12, updateCdf: true);
writer.WriteSymbol(false, Av1DefaultDistributions.Skip[0]);
writer.WriteSymbol(true, Av1DefaultDistributions.IntraInter[0]);
writer.WriteSymbol(true, Av1DefaultDistributions.CompInter[1]);
WriteCompoundReferencePair(writer, pairIndex: 1);
writer.WriteSymbol(0, Av1DefaultDistributions.InterCompoundMode[0]);
bool masked = compoundType is Av1CompoundType.Wedge or Av1CompoundType.DifferenceWeighted;
writer.WriteSymbol(masked, Av1DefaultDistributions.CompoundGroupIndex[0]);
if (masked)
{
writer.WriteSymbol(
compoundType == Av1CompoundType.Wedge ? 0 : 1,
Av1DefaultDistributions.CompoundType[(int)Av1BlockSize.Block8x8]);
if (compoundType == Av1CompoundType.Wedge)
{
writer.WriteSymbol(13, Av1DefaultDistributions.WedgeIndex[(int)Av1BlockSize.Block8x8]);
writer.WriteLiteral(true);
}
else
{
writer.WriteLiteral(true);
}
}
else
{
// Equal reference distances select context three; false chooses distance weighting.
writer.WriteSymbol(false, Av1DefaultDistributions.CompoundIndex[3]);
}
writer.WriteSymbol((int)Av1InterpolationFilter.Sharp, Av1DefaultDistributions.SwitchableInterpolation[3]);
using IMemoryOwner<byte> encoded = writer.Exit();
ReadInterFrameModeInfo(tileReader, encoded.Memory, modeInfo);
Assert.Equal(Av1ReferenceFrameType.Last, modeInfo.ReferenceFrames[0]);
Assert.Equal(Av1ReferenceFrameType.Last2, modeInfo.ReferenceFrames[1]);
Assert.Equal(masked, modeInfo.CompoundGroupIndex);
Assert.Equal(compoundType != Av1CompoundType.DistanceWeighted, modeInfo.CompoundIndex);
Assert.Equal(compoundType, modeInfo.CompoundType);
Assert.Equal(compoundType == Av1CompoundType.Wedge ? 13 : 0, modeInfo.CompoundWedgeIndex);
Assert.Equal(compoundType == Av1CompoundType.Wedge, modeInfo.CompoundWedgeSign);
Assert.Equal(
compoundType == Av1CompoundType.DifferenceWeighted
? Av1DifferenceWeightedMaskType.Type38Inverse
: Av1DifferenceWeightedMaskType.Type38,
modeInfo.DifferenceWeightedMaskType);
Assert.Equal(Av1InterpolationFilter.Sharp, modeInfo.InterpolationFilters[0]);
Assert.Equal(Av1InterpolationFilter.Sharp, modeInfo.InterpolationFilters[1]);
}
/// <summary>
/// Invokes the ref-struct mode parser with one available above neighbor.
/// </summary>

49
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionModeInfoTests.cs

@ -141,6 +141,55 @@ public class Av1MotionModeInfoTests
Assert.Equal(Av1InterpolationFilter.Sharp, modeInfo.InterpolationFilters[1]);
}
/// <summary>
/// Verifies selected smooth and wedge inter-intra syntax before switchable interpolation.
/// </summary>
/// <param name="useWedge">Whether the block selects an inter-intra wedge.</param>
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ReadInterFrameModeInfoReadsSelectedInterIntraBeforeInterpolation(bool useWedge)
{
ObuSequenceHeader sequenceHeader = CreateSequenceHeader();
sequenceHeader.EnableInterIntraCompound = true;
ObuFrameHeader frameHeader = CreateFrameHeader();
ConfigureForcedTranslationalGlobalMotion(frameHeader);
using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader);
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty);
Av1SuperblockInfo superblockInfo = new(tileReader.FrameInfo, Point.Empty);
Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None);
using Av1SymbolWriter writer = new(Configuration.Default, 6, updateCdf: true);
writer.WriteSymbol(false, Av1DefaultDistributions.Skip[0]);
writer.WriteSymbol(true, Av1DefaultDistributions.InterIntra[Av1BlockSize.Block8x8.GetSizeGroup()]);
writer.WriteSymbol(
(int)Av1InterIntraMode.Smooth,
Av1DefaultDistributions.InterIntraMode[Av1BlockSize.Block8x8.GetSizeGroup()]);
writer.WriteSymbol(useWedge, Av1DefaultDistributions.WedgeInterIntra[(int)Av1BlockSize.Block8x8]);
if (useWedge)
{
writer.WriteSymbol(13, Av1DefaultDistributions.WedgeIndex[(int)Av1BlockSize.Block8x8]);
}
writer.WriteSymbol((int)Av1InterpolationFilter.Sharp, Av1DefaultDistributions.SwitchableInterpolation[3]);
using IMemoryOwner<byte> encoded = writer.Exit();
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true);
tileReader.ReadInterFrameModeInfo(ref decoder, ref partitionInfo, new Av1TileInfo(0, 0, frameHeader));
Assert.Equal(Av1ReferenceFrameType.Last, modeInfo.ReferenceFrames[0]);
Assert.Equal(Av1ReferenceFrameType.Intra, modeInfo.ReferenceFrames[1]);
Assert.Equal(Av1InterIntraMode.Smooth, modeInfo.InterIntraMode);
Assert.Equal(useWedge, modeInfo.UseInterIntraWedge);
Assert.Equal(useWedge ? 13 : 0, modeInfo.InterIntraWedgeIndex);
Assert.Equal(Av1MotionMode.SimpleTranslation, modeInfo.MotionMode);
Assert.Equal(Av1InterpolationFilter.Sharp, modeInfo.InterpolationFilters[0]);
Assert.Equal(Av1InterpolationFilter.Sharp, modeInfo.InterpolationFilters[1]);
}
/// <summary>
/// Creates the monochrome 64x64 sequence geometry used by direct inter-mode syntax tests.
/// </summary>

292
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs

@ -138,6 +138,41 @@ public class Av1ReconstructionConformanceTests
/// </summary>
private const int AverageCompoundFixtureFrameCount = 19;
/// <summary>
/// The coverage bit representing distance-weighted compound prediction.
/// </summary>
private const int DistanceWeightedCompoundCoverage = 1 << 0;
/// <summary>
/// The coverage bit representing a non-inverted wedge compound mask.
/// </summary>
private const int WedgeCompoundCoverage = 1 << 1;
/// <summary>
/// The coverage bit representing an inverted wedge compound mask.
/// </summary>
private const int InvertedWedgeCompoundCoverage = 1 << 2;
/// <summary>
/// The coverage bit representing the first difference-weighted mask orientation.
/// </summary>
private const int DifferenceWeightedCompoundCoverage = 1 << 3;
/// <summary>
/// The coverage bit representing the inverted difference-weighted mask orientation.
/// </summary>
private const int InvertedDifferenceWeightedCompoundCoverage = 1 << 4;
/// <summary>
/// The coverage bit representing smooth inter-intra prediction.
/// </summary>
private const int SmoothInterIntraCoverage = 1 << 5;
/// <summary>
/// The coverage bit representing wedge inter-intra prediction.
/// </summary>
private const int WedgeInterIntraCoverage = 1 << 6;
/// <summary>
/// The hardware configurations covering the available vector widths and the scalar color-conversion fallback.
/// </summary>
@ -599,6 +634,263 @@ public class Av1ReconstructionConformanceTests
Assert.Equal(presentationReference is not null, presentationCompared);
}
/// <summary>
/// Verifies every selectable compound and inter-intra production branch against pinned native and presentation references.
/// </summary>
[Fact]
public void DecodeRealLibavifSequencesWithSelectableCompoundAndInterIntraMatchesPinnedReferences()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(
ValidateSelectableCompoundSequencesWithDefaultConfiguration,
ReconstructionConfigurations);
/// <summary>
/// Verifies selectable compound and inter-intra reconstruction through a constrained allocator.
/// </summary>
[Fact]
[ValidateDisposedMemoryAllocations]
public void DecodeRealLibavifSequencesWithSelectableCompoundAndInterIntraUseContiguousPlanes()
{
ValidateSelectableCompoundSequenceWithConstrainedAllocator(
TestImages.Heif.Av1DistanceWeightedCompoundSequenceAvif,
TestImages.Heif.Av1DistanceWeightedCompoundSequenceNativeReference,
TestImages.Heif.Av1DistanceWeightedCompoundSequencePresentationReference,
DistanceWeightedCompoundCoverage);
ValidateSelectableCompoundSequenceWithConstrainedAllocator(
TestImages.Heif.Av1WedgeCompoundSequenceAvif,
TestImages.Heif.Av1WedgeCompoundSequenceNativeReference,
TestImages.Heif.Av1WedgeCompoundSequencePresentationReference,
WedgeCompoundCoverage | InvertedWedgeCompoundCoverage);
ValidateSelectableCompoundSequenceWithConstrainedAllocator(
TestImages.Heif.Av1DifferenceWeightedCompoundSequenceAvif,
TestImages.Heif.Av1DifferenceWeightedCompoundSequenceNativeReference,
TestImages.Heif.Av1DifferenceWeightedCompoundSequencePresentationReference,
DifferenceWeightedCompoundCoverage | InvertedDifferenceWeightedCompoundCoverage);
ValidateSelectableCompoundSequenceWithConstrainedAllocator(
TestImages.Heif.Av1InterIntraSequenceAvif,
TestImages.Heif.Av1InterIntraSequenceNativeReference,
TestImages.Heif.Av1InterIntraSequencePresentationReference,
SmoothInterIntraCoverage | WedgeInterIntraCoverage);
}
/// <summary>
/// Verifies one complete selectable-compound sequence with a separately tracked constrained allocator.
/// </summary>
private static void ValidateSelectableCompoundSequenceWithConstrainedAllocator(
string imagePath,
string nativeReferencePath,
string presentationReferencePath,
int requiredCoverage)
{
TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 1_024 };
allocator.EnableNonThreadSafeLogging();
Configuration configuration = Configuration.Default.Clone();
configuration.MemoryAllocator = allocator;
ValidateSelectableCompoundSequence(
configuration,
imagePath,
nativeReferencePath,
presentationReferencePath,
requiredCoverage,
comparePresentation: false);
Assert.Contains(allocator.AllocationLog, request => request.ElementType.Name == "RetainedMotionFieldEntry");
Assert.Contains(allocator.AllocationLog, request => request.ElementType.Name == "TemporalMotionFieldEntry");
Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count);
Assert.All(
allocator.AllocationLog,
allocation => Assert.Single(
allocator.ReturnLog,
returned => returned.HashCodeOfBuffer == allocation.HashCodeOfBuffer));
}
/// <summary>
/// Runs every selectable compound fixture with exact final presentation comparison.
/// </summary>
private static void ValidateSelectableCompoundSequencesWithDefaultConfiguration()
=> ValidateSelectableCompoundSequences(Configuration.Default, comparePresentation: true);
/// <summary>
/// Validates every selectable compound fixture with the requested decoder configuration.
/// </summary>
/// <param name="configuration">The decoder configuration.</param>
/// <param name="comparePresentation">Whether to compare the final presented frame.</param>
private static void ValidateSelectableCompoundSequences(Configuration configuration, bool comparePresentation)
{
ValidateSelectableCompoundSequence(
configuration,
TestImages.Heif.Av1DistanceWeightedCompoundSequenceAvif,
TestImages.Heif.Av1DistanceWeightedCompoundSequenceNativeReference,
TestImages.Heif.Av1DistanceWeightedCompoundSequencePresentationReference,
DistanceWeightedCompoundCoverage,
comparePresentation);
ValidateSelectableCompoundSequence(
configuration,
TestImages.Heif.Av1WedgeCompoundSequenceAvif,
TestImages.Heif.Av1WedgeCompoundSequenceNativeReference,
TestImages.Heif.Av1WedgeCompoundSequencePresentationReference,
WedgeCompoundCoverage | InvertedWedgeCompoundCoverage,
comparePresentation);
ValidateSelectableCompoundSequence(
configuration,
TestImages.Heif.Av1DifferenceWeightedCompoundSequenceAvif,
TestImages.Heif.Av1DifferenceWeightedCompoundSequenceNativeReference,
TestImages.Heif.Av1DifferenceWeightedCompoundSequencePresentationReference,
DifferenceWeightedCompoundCoverage | InvertedDifferenceWeightedCompoundCoverage,
comparePresentation);
ValidateSelectableCompoundSequence(
configuration,
TestImages.Heif.Av1InterIntraSequenceAvif,
TestImages.Heif.Av1InterIntraSequenceNativeReference,
TestImages.Heif.Av1InterIntraSequencePresentationReference,
SmoothInterIntraCoverage | WedgeInterIntraCoverage,
comparePresentation);
}
/// <summary>
/// Decodes one complete retained-reference sequence and compares its final native and presented samples exactly.
/// </summary>
private static void ValidateSelectableCompoundSequence(
Configuration configuration,
string imagePath,
string nativeReferencePath,
string presentationReferencePath,
int requiredCoverage,
bool comparePresentation)
{
byte[] fileBytes = TestFile.Create(imagePath).Bytes;
byte[] referenceBytes = TestFile.Create(nativeReferencePath).Bytes;
ReadOnlySpan<byte> fileHeader =
"YUV4MPEG2 W80 H80 F25:1 Ip A0:0 C444 XYSCSS=444 XCOLORRANGE=LIMITED\n"u8;
ReadOnlySpan<byte> frameHeader = "FRAME\n"u8;
ReadOnlySpan<byte> nativeReference = referenceBytes;
Assert.True(nativeReference.StartsWith(fileHeader));
nativeReference = nativeReference[fileHeader.Length..];
Assert.True(nativeReference.StartsWith(frameHeader));
nativeReference = nativeReference[frameHeader.Length..];
Assert.Equal(AverageCompoundFixtureSize * AverageCompoundFixtureSize * 3, nativeReference.Length);
using Image<Rgba32> presentationReference =
Image.Load<Rgba32>(TestFile.Create(presentationReferencePath).Bytes);
HeifSequence sequence = ParseImageSequence(fileBytes);
HeifSequenceTrack track = sequence.ColorTrack;
int coverage = 0;
int visibleFrameCount = 0;
bool nativeCompared = false;
bool presentationCompared = false;
using Av1Decoder decoder = new(configuration);
for (int sampleIndex = 0; sampleIndex < track.Samples.Length; sampleIndex++)
{
HeifSequenceSample sample = track.Samples[sampleIndex];
Span<byte> sampleData = fileBytes.AsSpan((int)sample.Offset, sample.Length);
if (sample.IsHidden)
{
decoder.DecodeSequenceReference(
sampleData,
track.CicpProfile,
track.Av1CodecConfiguration);
coverage |= GetSelectableCompoundCoverage(decoder);
continue;
}
using ImageFrame<Rgba32> frame = decoder.DecodeSequenceFrame<Rgba32>(
sampleData,
track.CicpProfile,
track.Av1CodecConfiguration);
Av1FrameBuffer<byte> frameBuffer = Assert.IsType<Av1FrameBuffer<byte>>(decoder.FrameBuffer);
coverage |= GetSelectableCompoundCoverage(decoder);
// Every compound branch retains the same row-addressed plane contract under constrained allocators.
Assert.Equal(1, frameBuffer.BufferY!.FastMemoryGroup.Count);
Assert.Equal(1, frameBuffer.BufferCb!.FastMemoryGroup.Count);
Assert.Equal(1, frameBuffer.BufferCr!.FastMemoryGroup.Count);
if (visibleFrameCount == AverageCompoundFixtureFrameCount - 1)
{
AssertNativePlanesEqual(decoder, frameBuffer, nativeReference);
nativeCompared = true;
if (comparePresentation)
{
ImageSimilarityReport<Rgba32, Rgba32> report =
ImageComparer.Exact.CompareImagesOrFrames(
visibleFrameCount,
presentationReference.Frames.RootFrame,
frame);
Assert.True(report.IsEmpty, report.ToString());
presentationCompared = true;
}
}
visibleFrameCount++;
}
Assert.Equal(AverageCompoundFixtureFrameCount, visibleFrameCount);
Assert.Equal(requiredCoverage, coverage & requiredCoverage);
Assert.True(nativeCompared);
Assert.Equal(comparePresentation, presentationCompared);
}
/// <summary>
/// Collects the selectable compound and inter-intra modes retained in one decoded frame.
/// </summary>
private static int GetSelectableCompoundCoverage(Av1Decoder decoder)
{
ObuSequenceHeader sequenceHeader = Assert.IsType<ObuSequenceHeader>(decoder.SequenceHeader);
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
int superblockSizeLog2 = sequenceHeader.SuperblockSizeLog2;
int superblockColumnCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, superblockSizeLog2) >> superblockSizeLog2;
int superblockRowCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameHeight, superblockSizeLog2) >> superblockSizeLog2;
int coverage = 0;
for (int superblockRow = 0; superblockRow < superblockRowCount; superblockRow++)
{
for (int superblockColumn = 0; superblockColumn < superblockColumnCount; superblockColumn++)
{
Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(superblockColumn, superblockRow));
foreach (Av1BlockModeInfo modeInfo in superblockInfo.GetModeInfos())
{
if (modeInfo.ReferenceFrames[1] == Av1ReferenceFrameType.Intra)
{
coverage |= modeInfo.UseInterIntraWedge ? WedgeInterIntraCoverage : SmoothInterIntraCoverage;
continue;
}
if (modeInfo.ReferenceFrames[1] <= Av1ReferenceFrameType.Intra)
{
continue;
}
coverage |= modeInfo.CompoundType switch
{
Av1CompoundType.DistanceWeighted => DistanceWeightedCompoundCoverage,
Av1CompoundType.Wedge => modeInfo.CompoundWedgeSign
? InvertedWedgeCompoundCoverage
: WedgeCompoundCoverage,
Av1CompoundType.DifferenceWeighted => modeInfo.DifferenceWeightedMaskType == Av1DifferenceWeightedMaskType.Type38Inverse
? InvertedDifferenceWeightedCompoundCoverage
: DifferenceWeightedCompoundCoverage,
_ => 0,
};
}
}
}
return coverage;
}
/// <summary>
/// Verifies lossless syntax, residual reconstruction, and exact native samples against scalar libaom for
/// independently encoded eight-, ten-, and twelve-bit AVIF images.

154
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SelectableCompoundEntropyTests.cs

@ -0,0 +1,154 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
/// <summary>
/// Verifies the adaptive syntax distributions used by selectable compound and inter-intra prediction.
/// </summary>
[Trait("Format", "Avif")]
public class Av1SelectableCompoundEntropyTests
{
/// <summary>
/// Verifies representative and complete multi-symbol defaults against pinned libaom's forward Q15 tables.
/// </summary>
[Fact]
public void DefaultsMatchPinnedLibaom()
{
AssertForwardThresholds(Av1DefaultDistributions.InterIntraMode[1], [1875, 11082, 27332]);
AssertForwardThresholds(Av1DefaultDistributions.WedgeInterIntra[(int)Av1BlockSize.Block8x8], [20036]);
AssertForwardThresholds(Av1DefaultDistributions.CompoundType[(int)Av1BlockSize.Block8x8], [23431]);
AssertForwardThresholds(
Av1DefaultDistributions.WedgeIndex[(int)Av1BlockSize.Block8x8],
[2438, 4440, 6599, 8663, 11005, 12874, 15751, 18094, 20359, 22362, 24127, 25702, 27752, 29450, 31171]);
ReadOnlySpan<uint> compoundIndex = [18244, 12865, 7053, 13259, 9334, 4644];
ReadOnlySpan<uint> compoundGroupIndex = [26607, 22891, 18840, 24594, 19934, 22674];
for (int context = 0; context < compoundIndex.Length; context++)
{
AssertForwardThresholds(Av1DefaultDistributions.CompoundIndex[context], [compoundIndex[context]]);
AssertForwardThresholds(Av1DefaultDistributions.CompoundGroupIndex[context], [compoundGroupIndex[context]]);
}
}
/// <summary>
/// Verifies that every new reader selects and adapts its intended context without consuming adjacent syntax.
/// </summary>
[Fact]
public void ReadersRoundTripInNormativeOrder()
{
const int groupContext = 4;
const int compoundIndexContext = 2;
Av1BlockSize blockSize = Av1BlockSize.Block8x8;
using Av1SymbolWriter writer = new(Configuration.Default, 32, updateCdf: true);
writer.WriteSymbol((int)Av1InterIntraMode.Horizontal, Av1DefaultDistributions.InterIntraMode[blockSize.GetSizeGroup()]);
writer.WriteSymbol(true, Av1DefaultDistributions.WedgeInterIntra[(int)blockSize]);
writer.WriteSymbol(13, Av1DefaultDistributions.WedgeIndex[(int)blockSize]);
writer.WriteSymbol(true, Av1DefaultDistributions.CompoundGroupIndex[groupContext]);
writer.WriteSymbol(false, Av1DefaultDistributions.CompoundIndex[compoundIndexContext]);
writer.WriteSymbol(1, Av1DefaultDistributions.CompoundType[(int)blockSize]);
using IMemoryOwner<byte> encoded = writer.Exit();
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true);
Assert.Equal(Av1InterIntraMode.Horizontal, decoder.ReadInterIntraMode(blockSize));
Assert.True(decoder.ReadUseInterIntraWedge(blockSize));
Assert.Equal(13, decoder.ReadWedgeIndex(blockSize));
Assert.True(decoder.ReadCompoundGroupIndex(groupContext));
Assert.False(decoder.ReadCompoundIndex(compoundIndexContext));
Assert.Equal(Av1CompoundType.DifferenceWeighted, decoder.ReadMaskedCompoundType(blockSize));
}
/// <summary>
/// Verifies copying, resetting, and snapshot publication for every selectable-compound distribution family.
/// </summary>
[Fact]
public void FrameEntropyLifecycleIncludesSelectableCompoundFamilies()
{
Av1FrameEntropyContext source = new(0);
Av1FrameEntropyContext copy = new(0);
Av1FrameEntropyContext snapshot = new(0);
Av1FrameEntropyContext defaults = new(0);
Av1Distribution[] sourceDistributions =
[
source.InterIntraMode[1],
source.WedgeInterIntra[(int)Av1BlockSize.Block8x8],
source.CompoundType[(int)Av1BlockSize.Block8x8],
source.WedgeIndex[(int)Av1BlockSize.Block8x8],
source.CompoundIndex[2],
source.CompoundGroupIndex[4],
];
Av1Distribution[] copyDistributions =
[
copy.InterIntraMode[1],
copy.WedgeInterIntra[(int)Av1BlockSize.Block8x8],
copy.CompoundType[(int)Av1BlockSize.Block8x8],
copy.WedgeIndex[(int)Av1BlockSize.Block8x8],
copy.CompoundIndex[2],
copy.CompoundGroupIndex[4],
];
Av1Distribution[] snapshotDistributions =
[
snapshot.InterIntraMode[1],
snapshot.WedgeInterIntra[(int)Av1BlockSize.Block8x8],
snapshot.CompoundType[(int)Av1BlockSize.Block8x8],
snapshot.WedgeIndex[(int)Av1BlockSize.Block8x8],
snapshot.CompoundIndex[2],
snapshot.CompoundGroupIndex[4],
];
Av1Distribution[] defaultDistributions =
[
defaults.InterIntraMode[1],
defaults.WedgeInterIntra[(int)Av1BlockSize.Block8x8],
defaults.CompoundType[(int)Av1BlockSize.Block8x8],
defaults.WedgeIndex[(int)Av1BlockSize.Block8x8],
defaults.CompoundIndex[2],
defaults.CompoundGroupIndex[4],
];
foreach (Av1Distribution distribution in sourceDistributions)
{
distribution.Update(distribution.NumberOfSymbols - 1);
}
copy.CopyFrom(source);
source.SnapshotTo(snapshot);
for (int index = 0; index < sourceDistributions.Length; index++)
{
Assert.NotSame(sourceDistributions[index], copyDistributions[index]);
Assert.NotSame(sourceDistributions[index], snapshotDistributions[index]);
Assert.Equal(sourceDistributions[index][0], copyDistributions[index][0]);
Assert.Equal(sourceDistributions[index][0], snapshotDistributions[index][0]);
}
source.ResetToDefaults(0);
for (int index = 0; index < sourceDistributions.Length; index++)
{
Assert.Equal(defaultDistributions[index][0], sourceDistributions[index][0]);
}
}
/// <summary>
/// Compares one inverse-cumulative distribution with pinned forward thresholds.
/// </summary>
private static void AssertForwardThresholds(Av1Distribution distribution, ReadOnlySpan<uint> forwardThresholds)
{
Assert.Equal(forwardThresholds.Length + 1, distribution.NumberOfSymbols);
for (int index = 0; index < forwardThresholds.Length; index++)
{
Assert.Equal((uint)Av1Distribution.ProbabilityTop - forwardThresholds[index], distribution[index]);
}
}
}

12
tests/ImageSharp.Tests/TestImages.cs

@ -1360,6 +1360,18 @@ public static class TestImages
public const string Av1AverageCompoundSequenceAvif = "Heif/Av1/Conformance/libavif-webp-logo-average-compound.avif";
public const string Av1AverageCompoundSequenceNativeReference = "Heif/Av1/Conformance/libavif-webp-logo-average-compound-libaom.y4m";
public const string Av1AverageCompoundSequencePresentationReference = "Heif/Av1/Conformance/libavif-webp-logo-average-compound-libavif.png";
public const string Av1DistanceWeightedCompoundSequenceAvif = "Heif/Av1/Conformance/libavif-webp-logo-distance-weighted-compound.avif";
public const string Av1DistanceWeightedCompoundSequenceNativeReference = "Heif/Av1/Conformance/libavif-webp-logo-distance-weighted-compound-libaom.y4m";
public const string Av1DistanceWeightedCompoundSequencePresentationReference = "Heif/Av1/Conformance/libavif-webp-logo-distance-weighted-compound-libavif.png";
public const string Av1WedgeCompoundSequenceAvif = "Heif/Av1/Conformance/libavif-webp-logo-wedge-compound.avif";
public const string Av1WedgeCompoundSequenceNativeReference = "Heif/Av1/Conformance/libavif-webp-logo-wedge-compound-libaom.y4m";
public const string Av1WedgeCompoundSequencePresentationReference = "Heif/Av1/Conformance/libavif-webp-logo-wedge-compound-libavif.png";
public const string Av1DifferenceWeightedCompoundSequenceAvif = "Heif/Av1/Conformance/libavif-webp-logo-difference-weighted-compound.avif";
public const string Av1DifferenceWeightedCompoundSequenceNativeReference = "Heif/Av1/Conformance/libavif-webp-logo-difference-weighted-compound-libaom.y4m";
public const string Av1DifferenceWeightedCompoundSequencePresentationReference = "Heif/Av1/Conformance/libavif-webp-logo-difference-weighted-compound-libavif.png";
public const string Av1InterIntraSequenceAvif = "Heif/Av1/Conformance/libavif-webp-logo-inter-intra.avif";
public const string Av1InterIntraSequenceNativeReference = "Heif/Av1/Conformance/libavif-webp-logo-inter-intra-libaom.y4m";
public const string Av1InterIntraSequencePresentationReference = "Heif/Av1/Conformance/libavif-webp-logo-inter-intra-libavif.png";
public const string Av1Cdef8BitPayload = "Heif/Av1/Conformance/libaom-cdef-kodim23-8b.bit";
public const string Av1Cdef8BitReference = "Heif/Av1/Conformance/libaom-cdef-kodim23-8b-libaom.yuv";
public const string Av1Cdef8BitAvif = "Heif/Av1/Conformance/libavif-cdef-kodim23-8b.avif";

32
tests/Images/Input/Heif/Av1/Conformance/README.md

@ -19,6 +19,7 @@ The reference builds use `AOM_TARGET_CPU=generic` and disable libyuv. Native rec
- `.bit` files contain the exact AV1 elementary-stream payload used by reconstruction tests.
- `-libaom.yuv` files contain headerless planar Y, U, and V reference samples. Samples above eight bits are stored as little-endian 16-bit values.
- `-libaom-y4m.yuv` files retain the Y4M header together with the native planar frame.
- `-libaom.y4m` files retain the Y4M header together with the selected native sequence frame.
- `.png` files contain the eight-bit RGBA presentation reference produced by the pinned scalar libavif build.
## Coverage
@ -37,6 +38,10 @@ The reference builds use `AOM_TARGET_CPU=generic` and disable libyuv. Native rec
| `*-film-grain-*` | Full and restricted range, monochrome, identity matrix, 8/10/12-bit synthesis, overlap, and odd frame dimensions |
| `libavif-progressive-draw-points-8b` | A real two-layer color item whose final frame uses single-reference inter reconstruction, plus its progressive auxiliary alpha item |
| `libavif-webp-logo-average-compound` | A 19-frame YUV444 image sequence whose retained references reach equal-weight compound inter reconstruction |
| `libavif-webp-logo-distance-weighted-compound` | Selectable distance-weighted compound prediction |
| `libavif-webp-logo-wedge-compound` | Wedge compound prediction with both signaled mask orientations |
| `libavif-webp-logo-difference-weighted-compound` | Difference-weighted compound prediction with both mask orientations |
| `libavif-webp-logo-inter-intra` | Smooth and wedge inter-intra prediction |
The corresponding tests also assert the syntax required by each family before comparing output. This prevents an inactive tool or an incorrectly substituted stream from passing solely because its final pixels happen to match.
@ -69,6 +74,33 @@ Pinned scalar libavif generated the retained references with:
The AVIF SHA-256 is `7919049D367EEDB7C965E170309D6759660DDBFD4BB1AEF9496F9D66E314846A`. The retained frame-18 Y4M SHA-256 is `41FF2408DEB473D5483F3398882DF7F7AB6C7D376561C19798881595EB0C5C0C`, and the frame-18 PNG SHA-256 is `BCFABC1E1C7E17D8ECB40569849A04FFAC6CA1FCDF613F217B33816CA47337AC`. The test decodes every preceding hidden and shown sample to establish the same retained-reference state before comparing all native Y, U, and V samples and the final RGBA presentation.
## Selectable compound and inter-intra fixtures
The four selectable-compound fixtures use the same pinned `tests/data/webp_logo_animated.y4m` source and its `0872208D9C19B68B10A1647FA6849CFC4E2B21A19561ACD672E0629C70EFACA2` SHA-256. They were encoded at speed zero after disabling later inter-mode checkpoints. Each command also disables competing prediction tools that would prevent the resulting stream from isolating its named mode:
```text
./avifenc -j 1 -c aom -s 0 -q 80 -a enable-obmc=0 -a enable-warped-motion=0 -a enable-global-motion=0 -a enable-masked-comp=0 -a enable-interintra-comp=0 tests/data/webp_logo_animated.y4m libavif-webp-logo-distance-weighted-compound.avif
./avifenc -j 1 -c aom -s 0 -q 80 -a enable-obmc=0 -a enable-warped-motion=0 -a enable-global-motion=0 -a enable-dist-wtd-comp=0 -a enable-diff-wtd-comp=0 -a enable-interintra-comp=0 tests/data/webp_logo_animated.y4m libavif-webp-logo-wedge-compound.avif
./avifenc -j 1 -c aom -s 0 -q 80 -a enable-obmc=0 -a enable-warped-motion=0 -a enable-global-motion=0 -a enable-dist-wtd-comp=0 -a enable-interinter-wedge=0 -a enable-interintra-comp=0 tests/data/webp_logo_animated.y4m libavif-webp-logo-difference-weighted-compound.avif
./avifenc -j 1 -c aom -s 0 -q 80 -a enable-obmc=0 -a enable-warped-motion=0 -a enable-global-motion=0 -a enable-dist-wtd-comp=0 -a enable-masked-comp=0 tests/data/webp_logo_animated.y4m libavif-webp-logo-inter-intra.avif
```
Pinned scalar libavif generated each final native and presentation reference with:
```text
./avifdec -j 1 -c aom --index 18 <fixture>.avif <fixture>-libaom.y4m
./avifdec -j 1 -c aom --index 18 <fixture>.avif <fixture>-libavif.png
```
| Fixture | AVIF SHA-256 | Frame-18 Y4M SHA-256 | Frame-18 PNG SHA-256 |
| --- | --- | --- | --- |
| `libavif-webp-logo-distance-weighted-compound` | `DA710D11C60F03EEA209E4360E2FC807B89C49AD50671F0DFB1BCF4AD5EF76DD` | `904D1B5B3E7F334CE8D44040F9A7BDCAC1F7773122FF1C5F06A5B4DD31A62A97` | `D2CB388C9092EF17C4F0382C0150DD30D6F9D0EE247FF45AB5D7D4D312CEB23C` |
| `libavif-webp-logo-wedge-compound` | `98640640A445055FEA3D9E2F4A78FEEF54E97F8171B472CF57D99156E1C553B2` | `904D1B5B3E7F334CE8D44040F9A7BDCAC1F7773122FF1C5F06A5B4DD31A62A97` | `D2CB388C9092EF17C4F0382C0150DD30D6F9D0EE247FF45AB5D7D4D312CEB23C` |
| `libavif-webp-logo-difference-weighted-compound` | `FC6459CD334762D74D9D2654640221E80C463CC01B82B29A5866C9E725ABE273` | `904D1B5B3E7F334CE8D44040F9A7BDCAC1F7773122FF1C5F06A5B4DD31A62A97` | `D2CB388C9092EF17C4F0382C0150DD30D6F9D0EE247FF45AB5D7D4D312CEB23C` |
| `libavif-webp-logo-inter-intra` | `71DF22E63626B5BC9001FF1E88076B90F11BB47D18089750853E66F0CBBB084B` | `502265688138641A7B12C8C4190B66C76CD4808D9AED05B06486056B39D7E9A0` | `F0DE4CCDFB6D95A400E69B69FA4C57F0BEEEEE75825722C31613385F0B3FD9FC` |
Pinned libaom block tracing confirms that these streams select distance weighting, both wedge signs, both difference-mask types, and both smooth and wedge inter-intra prediction. The production test independently requires those decoded mode states, decodes all preceding samples, compares the final native Y, U, and V planes exactly, compares the final RGBA presentation exactly, and repeats reconstruction with constrained tracked allocation.
## Updating fixtures
Do not create conformance references with ImageSharp. Generate both the native-plane and presentation references with an independent decoder, record the exact upstream revisions and source license, and preserve exact comparisons. A new tool-specific fixture should demonstrate that the relevant syntax is active and should be no larger than required to cover that behavior.

3
tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-difference-weighted-compound-libaom.y4m

File diff suppressed because one or more lines are too long

3
tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-difference-weighted-compound-libavif.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d2cb388c9092ef17c4f0382c0150dd30d6f9d0ee247ff45ab5d7d4d312ceb23c
size 4544

3
tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-difference-weighted-compound.avif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fc6459cd334762d74d9d2654640221e80c463cc01b82b29a5866c9e725abe273
size 6423

3
tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-distance-weighted-compound-libaom.y4m

File diff suppressed because one or more lines are too long

3
tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-distance-weighted-compound-libavif.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d2cb388c9092ef17c4f0382c0150dd30d6f9d0ee247ff45ab5d7d4d312ceb23c
size 4544

3
tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-distance-weighted-compound.avif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:da710d11c60f03eea209e4360e2fc807b89c49ad50671f0dfb1bcf4ad5ef76dd
size 6437

3
tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-inter-intra-libaom.y4m

File diff suppressed because one or more lines are too long

3
tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-inter-intra-libavif.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f0de4ccdfb6d95a400e69b69fa4c57f0beeeee75825722c31613385f0b3fd9fc
size 4551

3
tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-inter-intra.avif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:71df22e63626b5bc9001ff1e88076b90f11bb47d18089750853e66f0cbbb084b
size 6392

3
tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-wedge-compound-libaom.y4m

File diff suppressed because one or more lines are too long

3
tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-wedge-compound-libavif.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d2cb388c9092ef17c4f0382c0150dd30d6f9d0ee247ff45ab5d7d4d312ceb23c
size 4544

3
tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-wedge-compound.avif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:98640640a445055fea3d9e2f4a78feef54e97f8171b472cf57d99156e1c553b2
size 6439
Loading…
Cancel
Save