Browse Source

Complete official AV1 motion-vector conformance

pull/2633/head
James Jackson-South 5 days ago
parent
commit
1c58d855f7
  1. 2
      .gitattributes
  2. 26
      HEIF_IMPLEMENTATION_PLAN.md
  3. 2
      src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs
  4. 529
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Intermediate.cs
  5. 732
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Compound.cs
  6. 5
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Dispatch.cs
  7. 32
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.OneDimension.cs
  8. 9
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.TwoDimensions.UInt16.cs
  9. 170
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Warped.cs
  10. 4
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs
  11. 468
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs
  12. 23
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs
  13. 57
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterPredictorTests.cs
  14. 180
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs
  15. 2
      tests/ImageSharp.Tests/TestImages.cs
  16. 3
      tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-05-mv-libaom.y4m
  17. 3
      tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-05-mv.ivf

2
.gitattributes

@ -140,6 +140,8 @@
*.avif filter=lfs diff=lfs merge=lfs -text *.avif filter=lfs diff=lfs merge=lfs -text
*.hvc1 filter=lfs diff=lfs merge=lfs -text *.hvc1 filter=lfs diff=lfs merge=lfs -text
*.hvcc filter=lfs diff=lfs merge=lfs -text *.hvcc filter=lfs diff=lfs merge=lfs -text
*.ivf filter=lfs diff=lfs merge=lfs -text
*.y4m filter=lfs diff=lfs merge=lfs -text
*.yuv filter=lfs diff=lfs merge=lfs -text *.yuv filter=lfs diff=lfs merge=lfs -text
*.bit filter=lfs diff=lfs merge=lfs -text *.bit filter=lfs diff=lfs merge=lfs -text
############################################################################### ###############################################################################

26
HEIF_IMPLEMENTATION_PLAN.md

@ -29,7 +29,9 @@ Checkboxes may be marked complete only when the implementation and the verificat
## Delivery dashboard ## Delivery dashboard
Last reconciled with the source tree on 2026-08-29 against committed production checkpoint `0b2db86809e26757460e719a6b59c0fe58bb0586` plus the locally complete global-warp 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, allocation-free SIMD-first equal averaging, selected inter-intra prediction, selectable compound blending, OBMC, scaled-reference reconstruction, and local warped prediction. The global-warp checkpoint reuses the committed affine filter for valid rotation/zoom and affine `GLOBALMV` models while preserving translational fallback for identity, translation, invalid shear, scaled references, integer-only frames, OBMC, and sub-8x8 plane predictions. Its genuine two-frame 256x256 bounded AVIF contains seven pinned-libaom-traced rotation/zoom `GLOBALMV` blocks and passes exact final pinned-libaom YUV444 planes, exact pinned-libavif RGBA presentation, normal and scalar `FeatureTestRunner` execution, constrained allocation, and balanced ownership tracking. A direct production-branch test drives both matrices of `GLOBAL_GLOBALMV` through compound averaging at 8, 10, and 12 bits. Both source targets build with zero warnings and errors; the `net10.0` test project builds with zero errors and pre-existing warnings; 84 focused `net10.0` cases pass without failures or skips; and the documentation and diff checks are complete. 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 `c5637ea0187df35b385bf43e2fe85cd955f01099`. 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, allocation-free SIMD-first equal averaging, selected inter-intra prediction, selectable compound blending, OBMC, scaled-reference reconstruction, local warped prediction, and non-translational global prediction. The global-warp checkpoint reuses the committed affine filter for valid rotation/zoom and affine `GLOBALMV` models while preserving translational fallback for identity, translation, invalid shear, scaled references, integer-only frames, OBMC, and sub-8x8 plane predictions. Its genuine two-frame 256x256 bounded AVIF contains seven pinned-libaom-traced rotation/zoom `GLOBALMV` blocks and passes exact final pinned-libaom YUV444 planes, exact pinned-libavif RGBA presentation, normal and scalar `FeatureTestRunner` execution, constrained allocation, and balanced ownership tracking. A direct production-branch test drives both matrices of `GLOBAL_GLOBALMV` through compound averaging at 8, 10, and 12 bits. Both source targets build with zero warnings and errors; the `net10.0` test project builds with zero errors and pre-existing warnings; 84 focused `net10.0` cases pass without failures or skips; and the documentation and diff checks are complete. 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.
The working tree contains a complete, verified official motion-vector conformance checkpoint awaiting commit. The official IVF has SHA-1 `F064290D7FCD3B3DE19020E8AEC6C43C88D3A505`, matching the pinned libaom test-data manifest, and SHA-256 `222A9050059B254DAB17CFB802FF829C778E3F93AF18961A622C8268576C1395`; its pinned-libaom Y4M has SHA-256 `D97AC78C81782CF1507549368047769DC677DBE205706458D1EE9C807DE6EC78`. Both source targets build with zero warnings and errors, the `net10.0` test-project analyzer build completes with zero errors and 1,014 pre-existing repository warnings, Roslynk reports zero compiler errors, 3,985 focused `net10.0` cases pass without failures or skips, and `git diff --check` is clean.
Status meanings: Status meanings:
@ -39,15 +41,15 @@ Status meanings:
- **Not started:** supporting primitives may exist, but the production format path is absent. - **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:** 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, and reconstructs translational single-reference, compound, inter-intra, OBMC, scaled-reference, local warped, and non-translational global prediction before residual traversal. The global-warp implementation and independent evidence are complete locally; its checkpoint commit is the only remaining gate before the next recorded AV1 decoding item becomes current. 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, and reconstructs translational single-reference, compound, inter-intra, OBMC, scaled-reference, local warped, and non-translational global prediction before residual traversal. Commit `c5637ea0187df35b385bf43e2fe85cd955f01099` records the global-warp checkpoint. The working tree additionally decodes the official four-frame motion-vector sequence exactly through all ordinary inter modes, all three motion modes, every regular/smooth/sharp dual-filter pair, sub-8x8 chroma prediction, and no-round compound intermediates. Neither AV1 nor HEVC production encoding is implemented.
Immediate checkpoint: **commit the complete non-translational global-motion implementation and its verified evidence before advancing to another AV1 decoding item.** The production branch, single and compound matrix reconstruction, independent native and presentation comparisons, constrained allocator coverage, Release builds, focused tests, documentation review, and diff validation are complete. Immediate checkpoint: **commit the verified official motion-vector conformance slice before advancing.** Exact official native-plane comparison, normal/scalar dispatch, constrained allocation, sub-8x8 chroma, no-round compound arithmetic, Release builds, Roslyn/compiler checks, the focused suite, documentation review, and the diff gate all pass.
| Order | Delivery stage | State | Delivered state | Gate that remains open | | 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. | | 1 | Baseline, provenance, documentation, and public contract | In progress | Pinned codec references, a bounded image-only scope, encoder options, typed bit depth, decoder-option propagation, and extensive HEIF documentation exist. | Complete the all-file documentation audit, record a fresh Release baseline, finish distinct public HEIC/AVIF save boundaries, and close API review. |
| 2 | Bounded HEIF item and image-sequence container | In progress | Still-item parsing, grids, auxiliary alpha, metadata properties, bounded image-sequence tracks, Identify, and all-sync AV1 sequence presentation are connected. | Complete adversarial boundary coverage, remaining item/property behavior, reference-dependent sequence reconstruction, and the bounded sequence writer. | | 2 | Bounded HEIF item and image-sequence container | In progress | Still-item parsing, grids, auxiliary alpha, metadata properties, bounded image-sequence tracks, Identify, and all-sync AV1 sequence presentation are connected. | Complete adversarial boundary coverage, remaining item/property behavior, reference-dependent sequence reconstruction, and the bounded sequence writer. |
| 3 | Still-image AV1 and HEVC decoding | **Current** | HEVC reconstruction reaches exact HM/libheif fixtures across the recorded 8/10/12-bit and chroma cases. AV1 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 and selectable compound reconstruction, inter-intra prediction, committed OBMC, scaled-reference reconstruction, local warped prediction, and locally complete independently verified non-translational global prediction. | Commit 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 and selectable compound reconstruction, inter-intra prediction, committed OBMC, scaled-reference reconstruction, local warped prediction, 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. | | 4 | Complete decoded presentation and animation | In progress | Shared SIMD-first AV1/HEVC color conversion, ICC application, grids, transforms, direct planar alpha composition, frame metadata, repetition, and independently decodable AV1 sequence samples exist. | Close the full color/ICC cross-product, HEVC sequence decoding, AV1/HEVC reference-dependent samples, frame-local metadata/alpha behavior, and independent animated decode vectors. |
| 5 | AV1/AVIF encoding | Not started | RGB-to-planar conversion, forward transforms, OBU writer foundations, options, and container-writing infrastructure exist. | `HeifEncoderCore` still rejects AV1. Implement a real independently decodable lossy/lossless AV1 payload and the complete AVIF item/metadata matrix. | | 5 | AV1/AVIF encoding | Not started | RGB-to-planar conversion, forward transforms, OBU writer foundations, options, and container-writing infrastructure exist. | `HeifEncoderCore` still rejects AV1. Implement a real independently decodable lossy/lossless AV1 payload and the complete AVIF item/metadata matrix. |
| 6 | HEVC/HEIC encoding | Not started | Shared input color conversion, options, and HEIF writer infrastructure exist. | `HeifEncoderCore` still rejects HEVC. Implement a real independently decodable lossy/lossless HEVC payload and the complete HEIC item/metadata matrix. | | 6 | HEVC/HEIC encoding | Not started | Shared input color conversion, options, and HEIF writer infrastructure exist. | `HeifEncoderCore` still rejects HEVC. Implement a real independently decodable lossy/lossless HEVC payload and the complete HEIC item/metadata matrix. |
@ -93,8 +95,9 @@ Immediate checkpoint: **commit the complete non-translational global-motion impl
- [x] Decode and reconstruct above and left OBMC through allocator-owned prediction workspaces, exact pinned masks, and the existing SIMD-first masked blender. Commit `59a4fbad5ed3bc7c34331c4bdc65acc7acde3f04` records direct 8/10/12-bit production reconstruction, 4:2:0 and 4:2:2 overlap geometry, a real 19-frame YUV444 sequence containing more than one hundred traced OBMC blocks, exact final pinned-libaom native planes, exact pinned-libavif presentation through normal and scalar `FeatureTestRunner` configurations, constrained tracked allocation, zero-warning `net10.0` and `net11.0` source builds, a zero-error analyzer build with 1,013 pre-existing warnings, 14 direct focused cases plus the feature-isolated comparison, and clean staged `git diff --check`. - [x] Decode and reconstruct above and left OBMC through allocator-owned prediction workspaces, exact pinned masks, and the existing SIMD-first masked blender. Commit `59a4fbad5ed3bc7c34331c4bdc65acc7acde3f04` records direct 8/10/12-bit production reconstruction, 4:2:0 and 4:2:2 overlap geometry, a real 19-frame YUV444 sequence containing more than one hundred traced OBMC blocks, exact final pinned-libaom native planes, exact pinned-libavif presentation through normal and scalar `FeatureTestRunner` configurations, constrained tracked allocation, zero-warning `net10.0` and `net11.0` source builds, a zero-error analyzer build with 1,013 pre-existing warnings, 14 direct focused cases plus the feature-isolated comparison, and clean staged `git diff --check`.
- [x] Decode and independently verify scaled-reference prediction through exact Q14/Q10 arithmetic, allocation-free SIMD/scalar convolution, 8/10/12-bit production reconstruction, scaled OBMC, active inter deblocking, constrained allocation, and a genuine size-changing bounded AVIF. - [x] Decode and independently verify scaled-reference prediction through exact Q14/Q10 arithmetic, allocation-free SIMD/scalar convolution, 8/10/12-bit production reconstruction, scaled OBMC, active inter deblocking, constrained allocation, and a genuine size-changing bounded AVIF.
- [x] Decode local warped motion through pinned integer projection and allocation-free SIMD-first affine filtering. A genuine two-frame bounded AVIF selects traced multi-sample warped blocks and passes exact final native-plane and presentation comparisons, constrained allocation, and normal/scalar dispatch. Commit `0b2db86809e26757460e719a6b59c0fe58bb0586` records the checkpoint. - [x] Decode local warped motion through pinned integer projection and allocation-free SIMD-first affine filtering. A genuine two-frame bounded AVIF selects traced multi-sample warped blocks and passes exact final native-plane and presentation comparisons, constrained allocation, and normal/scalar dispatch. Commit `0b2db86809e26757460e719a6b59c0fe58bb0586` records the checkpoint.
- [x] Decode non-translational global motion through the committed affine filter. A genuine two-frame bounded AVIF selects seven traced rotation/zoom `GLOBALMV` blocks and passes exact final native-plane and presentation comparisons, constrained allocation, and normal/scalar dispatch. Direct 8/10/12-bit production reconstruction covers both matrices of compound `GLOBAL_GLOBALMV`. The checkpoint is complete locally and awaiting commit. - [x] Decode non-translational global motion through the committed affine filter. A genuine two-frame bounded AVIF selects seven traced rotation/zoom `GLOBALMV` blocks and passes exact final native-plane and presentation comparisons, constrained allocation, and normal/scalar dispatch. Direct 8/10/12-bit production reconstruction covers both matrices of compound `GLOBAL_GLOBALMV`. Commit `c5637ea0187df35b385bf43e2fe85cd955f01099` records the checkpoint.
- [ ] **Queued until the global-warp commit:** verify every remaining connected mode and filter with independently encoded bounded AV1 content and exact native-plane comparisons. - [x] Decode the official `av1-1-b8-05-mv.ivf` sequence through every ordinary inter mode, all three motion modes, and every regular/smooth/sharp dual-filter pair; reconstruct sub-8x8 chroma and allocation-free no-round compound intermediates; and compare all four shown 352x288 YUV420 frames exactly with pinned-libaom output under normal/scalar dispatch and constrained tracked allocation. The official IVF SHA-1 matches the pinned libaom manifest. Both Release source targets build with zero warnings and errors; the `net10.0` test-project analyzer build completes with zero errors and 1,014 pre-existing repository warnings; Roslynk reports zero compiler errors; 3,983 focused decoder/predictor cases plus two complete official-sequence cases pass without failures or skips; and `git diff --check` is clean. The checkpoint is complete locally and awaiting commit.
- [ ] **Queued until the official motion-vector checkpoint commit:** 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. - [ ] Return the explicitly selected spatial layer or the final displayed layer, keeping reference reconstruction separate from display-only film grain.
- [ ] Verify color and auxiliary-alpha output exactly against both pinned libavif progressive fixtures under normal SIMD dispatch and all required `FeatureTestRunner` fallbacks. - [ ] Verify color and auxiliary-alpha output exactly against both pinned libavif progressive fixtures under normal SIMD dispatch and all required `FeatureTestRunner` fallbacks.
- [ ] Correct the audited 12-bit inverse ADST4, Identity4, and Identity16 SIMD arithmetic by widening only the libaom-widened multiply/accumulate operations, with exact conformant-range vectors and `FeatureTestRunner` coverage. - [ ] Correct the audited 12-bit inverse ADST4, Identity4, and Identity16 SIMD arithmetic by widening only the libaom-widened multiply/accumulate operations, with exact conformant-range vectors and `FeatureTestRunner` coverage.
@ -399,7 +402,7 @@ This assessment was reconciled with the source tree on 2026-08-29, including pro
- Commit `59a4fbad5ed3bc7c34331c4bdc65acc7acde3f04` reconstructs eligible above and left OBMC neighbors through the retained primary reference, exact libaom masks, and the existing SIMD-first masked blender without per-block allocation. Direct production tests cover 8/10/12-bit storage plus 4:2:0 and 4:2:2 plane geometry. A pinned 19-frame sequence contains more than one hundred traced OBMC blocks and passes exact final native-plane, final-presentation, constrained-allocation, normal-dispatch, and scalar-dispatch comparison. - Commit `59a4fbad5ed3bc7c34331c4bdc65acc7acde3f04` reconstructs eligible above and left OBMC neighbors through the retained primary reference, exact libaom masks, and the existing SIMD-first masked blender without per-block allocation. Direct production tests cover 8/10/12-bit storage plus 4:2:0 and 4:2:2 plane geometry. A pinned 19-frame sequence contains more than one hundred traced OBMC blocks and passes exact final native-plane, final-presentation, constrained-allocation, normal-dispatch, and scalar-dispatch comparison.
- Commit `8f57c29b250b4291f034b31c1ea3de3fa3345467` maps current-frame Q4 positions into retained-frame Q10 coordinates with pinned-libaom Q14 scale factors and signed rounding, then applies allocation-free variable-phase two-pass convolution across byte and high-bit-depth storage. The production block path selects scaling from actual retained/current dimensions, covers both primary and secondary workspaces plus scaled OBMC neighbors, and retains libaom's 288-sample prediction border. The real layered fixture also exposed and now verifies per-block inter loop-filter reference/mode deltas and skipped-transform edge decisions that the earlier intra-only deblocking integration did not model. - Commit `8f57c29b250b4291f034b31c1ea3de3fa3345467` maps current-frame Q4 positions into retained-frame Q10 coordinates with pinned-libaom Q14 scale factors and signed rounding, then applies allocation-free variable-phase two-pass convolution across byte and high-bit-depth storage. The production block path selects scaling from actual retained/current dimensions, covers both primary and secondary workspaces plus scaled OBMC neighbors, and retains libaom's 288-sample prediction border. The real layered fixture also exposed and now verifies per-block inter loop-filter reference/mode deltas and skipped-transform edge decisions that the earlier intra-only deblocking integration did not model.
- Commit `0b2db86809e26757460e719a6b59c0fe58bb0586` derives AV1's local affine matrix from fixed-capacity spatial samples with pinned integer least squares, reciprocal lookup, coefficient clamping, and shear validation. Production reconstruction applies the pinned 193-phase Q7 warped filter in allocation-free 8x8 tiles through `Vector128` convolution with scalar fallback for 8/10/12-bit planes; an invalid selected model falls back to translational prediction as libaom does. - Commit `0b2db86809e26757460e719a6b59c0fe58bb0586` derives AV1's local affine matrix from fixed-capacity spatial samples with pinned integer least squares, reciprocal lookup, coefficient clamping, and shear validation. Production reconstruction applies the pinned 193-phase Q7 warped filter in allocation-free 8x8 tiles through `Vector128` convolution with scalar fallback for 8/10/12-bit planes; an invalid selected model falls back to translational prediction as libaom does.
- The locally complete global-warp checkpoint selects the full frame-header rotation/zoom or affine matrix independently for each `GLOBAL_GLOBALMV` reference, while identity, translation, invalid shear, scaled references, integer-only frames, OBMC, and sub-8x8 plane predictions retain their normative translational path. A pinned bounded sequence proves seven actual single-reference `GLOBALMV` blocks through exact native and presentation output, and a direct `FeatureTestRunner` production test proves both compound matrices at 8, 10, and 12 bits. - Commit `c5637ea0187df35b385bf43e2fe85cd955f01099` selects the full frame-header rotation/zoom or affine matrix independently for each `GLOBAL_GLOBALMV` reference, while identity, translation, invalid shear, scaled references, integer-only frames, OBMC, and sub-8x8 plane predictions retain their normative translational path. A pinned bounded sequence proves seven actual single-reference `GLOBALMV` blocks through exact native and presentation output, and a direct `FeatureTestRunner` production test proves both compound matrices at 8, 10, and 12 bits.
- 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. - 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. - 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. - 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.
@ -583,8 +586,9 @@ Implement and verify in dependency order:
- [x] Decode and reconstruct OBMC through above and left neighboring prediction, exact libaom masks, allocator-owned workspaces, and the existing SIMD-first masked blender. Commit `59a4fbad5ed3bc7c34331c4bdc65acc7acde3f04` records exact pinned native/presentation evidence, direct 8/10/12-bit and subsampled reconstruction, constrained allocation, both Release source builds, and normal/scalar dispatch. - [x] Decode and reconstruct OBMC through above and left neighboring prediction, exact libaom masks, allocator-owned workspaces, and the existing SIMD-first masked blender. Commit `59a4fbad5ed3bc7c34331c4bdc65acc7acde3f04` records exact pinned native/presentation evidence, direct 8/10/12-bit and subsampled reconstruction, constrained allocation, both Release source builds, and normal/scalar dispatch.
- [x] Decode scaled references through exact Q14 scale factors, signed Q4-to-Q10 coordinate rounding, variable per-output phases and steps, regular/smooth/sharp/bilinear reduced and full filters, bit-depth-dependent two-pass rounding, scaled OBMC neighbors, and the normative 288-sample retained border without per-block allocation. The independently generated 40x40-to-80x80 layered fixture proves the base reference before retention and in the production retained session, then matches the final pinned-libaom Y/U/V planes and pinned-libavif RGBA presentation exactly. Active inter deblocking additionally verifies per-block reference/mode deltas and skipped inter/intra boundary decisions. Normal and scalar `FeatureTestRunner` runs, constrained contiguous planes, unique allocation identities, 8/10/12-bit production-block coverage, zero-warning `net10.0` and `net11.0` source builds, the zero-error test-project build with 1,013 pre-existing warnings, 87 focused cases, documentation review, and `git diff --check` pass. - [x] Decode scaled references through exact Q14 scale factors, signed Q4-to-Q10 coordinate rounding, variable per-output phases and steps, regular/smooth/sharp/bilinear reduced and full filters, bit-depth-dependent two-pass rounding, scaled OBMC neighbors, and the normative 288-sample retained border without per-block allocation. The independently generated 40x40-to-80x80 layered fixture proves the base reference before retention and in the production retained session, then matches the final pinned-libaom Y/U/V planes and pinned-libavif RGBA presentation exactly. Active inter deblocking additionally verifies per-block reference/mode deltas and skipped inter/intra boundary decisions. Normal and scalar `FeatureTestRunner` runs, constrained contiguous planes, unique allocation identities, 8/10/12-bit production-block coverage, zero-warning `net10.0` and `net11.0` source builds, the zero-error test-project build with 1,013 pre-existing warnings, 87 focused cases, documentation review, and `git diff --check` pass.
- [x] Decode local warped motion through pinned integer projection and allocation-free 8x8-tiled affine filtering across byte and high-bit-depth planes. Commit `0b2db86809e26757460e719a6b59c0fe58bb0586` records exact traced native/presentation evidence, constrained allocation, and normal/scalar dispatch. - [x] Decode local warped motion through pinned integer projection and allocation-free 8x8-tiled affine filtering across byte and high-bit-depth planes. Commit `0b2db86809e26757460e719a6b59c0fe58bb0586` records exact traced native/presentation evidence, constrained allocation, and normal/scalar dispatch.
- [x] Decode non-translational global motion through reconstructed reference planes. The complete local checkpoint covers pinned single-reference rotation/zoom syntax and reconstruction, both compound reference workspaces, 8/10/12-bit storage, exact native and presentation output, constrained allocation, balanced ownership, and normal/scalar dispatch; it awaits commit. - [x] Decode non-translational global motion through reconstructed reference planes. Commit `c5637ea0187df35b385bf43e2fe85cd955f01099` records pinned single-reference rotation/zoom syntax and reconstruction, both compound reference workspaces, 8/10/12-bit storage, exact native and presentation output, constrained allocation, balanced ownership, and normal/scalar dispatch.
- [ ] **Queued until the global-warp commit:** verify every remaining connected inter mode and filter with independently encoded bounded AV1 content and exact native-plane comparisons. - [x] Decode all ordinary inter modes, motion modes, and regular/smooth/sharp dual-filter pairs from the official four-frame 352x288 YUV420 motion-vector sequence. The production path includes sub-8x8 chroma assembly and allocation-free no-round compound intermediates. Exact pinned-libaom native planes, normal/scalar dispatch, constrained tracked allocation, direct compound predictor parity, zero-warning source builds, the zero-error analyzer build, Roslyn compiler diagnostics, 3,985 focused cases, documentation review, and `git diff --check` pass. The checkpoint is complete locally and awaiting commit.
- [ ] **Queued until this checkpoint commits:** 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. - [ ] Lossless and high-bit-depth reconstruction with correct clipping and intermediate precision.
- [x] Route lossless 4x4 blocks through allocation-free reversible inverse Walsh-Hadamard reconstruction for 8/10/12-bit samples, including the DC-only specialization, `Vector128` production traversal, scalar fallback, exact clipping, and `FeatureTestRunner` parity. - [x] Route lossless 4x4 blocks through allocation-free reversible inverse Walsh-Hadamard reconstruction for 8/10/12-bit samples, including the DC-only specialization, `Vector128` production traversal, scalar fallback, exact clipping, and `FeatureTestRunner` parity.
- [x] Verify lossless syntax, inverse quantization, prediction, and presented reconstruction with independently encoded 8/10/12-bit AVIF fixtures. The tests require coded residuals with palette and intra-block copy disabled, compare every native YUV sample with the pinned generic libaom-backed decoder, and compare every presented RGBA byte with pinned generic libavif exactly under normal hardware dispatch and the scalar fallback. - [x] Verify lossless syntax, inverse quantization, prediction, and presented reconstruction with independently encoded 8/10/12-bit AVIF fixtures. The tests require coded residuals with palette and intra-block copy disabled, compare every native YUV sample with the pinned generic libaom-backed decoder, and compare every presented RGBA byte with pinned generic libavif exactly under normal hardware dispatch and the scalar fallback.
@ -826,8 +830,8 @@ The dashboard and immediate execution queue define the remaining critical path.
- [x] Implement and independently verify OBMC through exact traced native-plane and presentation evidence, direct 8/10/12-bit and subsampled production coverage, constrained allocation, and normal/scalar dispatch. Commit `59a4fbad5ed3bc7c34331c4bdc65acc7acde3f04` records the checkpoint. - [x] Implement and independently verify OBMC through exact traced native-plane and presentation evidence, direct 8/10/12-bit and subsampled production coverage, constrained allocation, and normal/scalar dispatch. Commit `59a4fbad5ed3bc7c34331c4bdc65acc7acde3f04` records the checkpoint.
- [x] Implement and independently verify scaled references through pinned fixed-point oracles, direct 8/10/12-bit production reconstruction, a genuine 40x40-to-80x80 bounded AVIF, exact retained-base/final-native/final-presentation comparisons, active inter deblocking, constrained allocation, and normal/scalar dispatch. Commit `8f57c29b250b4291f034b31c1ea3de3fa3345467` records the checkpoint. - [x] Implement and independently verify scaled references through pinned fixed-point oracles, direct 8/10/12-bit production reconstruction, a genuine 40x40-to-80x80 bounded AVIF, exact retained-base/final-native/final-presentation comparisons, active inter deblocking, constrained allocation, and normal/scalar dispatch. Commit `8f57c29b250b4291f034b31c1ea3de3fa3345467` records the checkpoint.
- [x] Implement and independently verify local warped motion through pinned projection, direct 8/10/12-bit predictor coverage, a genuine traced two-frame bounded AVIF, exact final native and presentation comparisons, constrained allocation, and normal/scalar dispatch. Commit `0b2db86809e26757460e719a6b59c0fe58bb0586` records the checkpoint. - [x] Implement and independently verify local warped motion through pinned projection, direct 8/10/12-bit predictor coverage, a genuine traced two-frame bounded AVIF, exact final native and presentation comparisons, constrained allocation, and normal/scalar dispatch. Commit `0b2db86809e26757460e719a6b59c0fe58bb0586` records the checkpoint.
- [x] Implement and independently verify non-translational global prediction with a genuine traced bounded sequence, exact native and presentation comparisons, constrained allocation, normal/scalar dispatch, and direct 8/10/12-bit compound production coverage. The checkpoint is complete locally and awaiting commit. - [x] Implement and independently verify non-translational global prediction with a genuine traced bounded sequence, exact native and presentation comparisons, constrained allocation, normal/scalar dispatch, and direct 8/10/12-bit compound production coverage. Commit `c5637ea0187df35b385bf43e2fe85cd955f01099` records the checkpoint.
- [ ] **Queued until the global-warp commit:** 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. - [ ] **Current:** 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 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. - [ ] 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.
- [ ] Close the still-image portions of Phases 0, 1, and 2 that remain as release gates: documentation, provenance, public format boundaries, API review, parser hardening, and malformed-input coverage. - [ ] Close the still-image portions of Phases 0, 1, and 2 that remain as release gates: documentation, provenance, public format boundaries, API review, parser hardening, and malformed-input coverage.

2
src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs

@ -194,7 +194,7 @@ internal static class Av1Constants
/// <summary> /// <summary>
/// The number of low-order bits reserved for a cumulative coefficient-level context. /// The number of low-order bits reserved for a cumulative coefficient-level context.
/// </summary> /// </summary>
public const int CoefficientContextBitCount = 6; public const int CoefficientContextBitCount = 3;
/// <summary> /// <summary>
/// The mask selecting the cumulative coefficient-level magnitude bits. /// The mask selecting the cumulative coefficient-level magnitude bits.

529
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Intermediate.cs

@ -0,0 +1,529 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
/// <content>
/// Combines high-precision compound convolution intermediates into reconstructed samples.
/// </content>
internal static partial class Av1CompoundInterPredictor
{
/// <summary>
/// Combines two compound intermediates by equal averaging.
/// </summary>
public static void AverageIntermediate(
Span<byte> destination,
int destinationStride,
ReadOnlySpan<ushort> first,
int firstStride,
ReadOnlySpan<ushort> second,
int secondStride,
int width,
int height,
int bitDepth)
{
GetIntermediateRounding(bitDepth, out int roundBits, out int roundOffset);
for (int row = 0; row < height; row++)
{
Span<byte> destinationRow = destination.Slice(row * destinationStride, width);
ReadOnlySpan<ushort> firstRow = first.Slice(row * firstStride, width);
ReadOnlySpan<ushort> secondRow = second.Slice(row * secondStride, width);
ref byte destinationReference = ref MemoryMarshal.GetReference(destinationRow);
ref ushort firstReference = ref MemoryMarshal.GetReference(firstRow);
ref ushort secondReference = ref MemoryMarshal.GetReference(secondRow);
int column = 0;
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));
AverageIntermediate(first0, first1, second0, second1, roundBits, roundOffset).StoreUnsafe(
ref destinationReference,
(nuint)column);
}
}
for (; column < width; column++)
{
// The reference average deliberately truncates here. The sole rounding step follows bias removal,
// preventing the double rounding that occurs when each reference is first converted to pixels.
int result = ((firstRow[column] + secondRow[column]) >> 1) - roundOffset;
destinationRow[column] = (byte)Math.Clamp(RoundPowerOfTwo(result, roundBits), 0, byte.MaxValue);
}
}
}
/// <summary>
/// Combines two compound intermediates using the decoded temporal-distance weights.
/// </summary>
public static void DistanceWeightedIntermediate(
Span<byte> destination,
int destinationStride,
ReadOnlySpan<ushort> first,
int firstStride,
ReadOnlySpan<ushort> second,
int secondStride,
int width,
int height,
int firstWeight,
int secondWeight,
int bitDepth)
{
GetIntermediateRounding(bitDepth, out int roundBits, out int roundOffset);
for (int row = 0; row < height; row++)
{
Span<byte> destinationRow = destination.Slice(row * destinationStride, width);
ReadOnlySpan<ushort> firstRow = first.Slice(row * firstStride, width);
ReadOnlySpan<ushort> secondRow = second.Slice(row * secondStride, width);
ref byte destinationReference = ref MemoryMarshal.GetReference(destinationRow);
ref ushort firstReference = ref MemoryMarshal.GetReference(firstRow);
ref ushort secondReference = ref MemoryMarshal.GetReference(secondRow);
int column = 0;
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));
DistanceWeightedIntermediate(
first0,
first1,
second0,
second1,
firstWeight,
secondWeight,
roundBits,
roundOffset).StoreUnsafe(ref destinationReference, (nuint)column);
}
}
for (; column < width; column++)
{
int result = ((firstRow[column] * firstWeight) + (secondRow[column] * secondWeight)) >> DistanceWeightBits;
result -= roundOffset;
destinationRow[column] = (byte)Math.Clamp(RoundPowerOfTwo(result, roundBits), 0, byte.MaxValue);
}
}
}
/// <summary>
/// Fills a luma-resolution difference-weighted mask from compound intermediates.
/// </summary>
public static void FillDifferenceWeightedIntermediateMask(
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;
GetIntermediateRounding(bitDepth, out int roundBits, out _);
int differenceRound = roundBits + bitDepth - 8;
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;
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));
DifferenceWeightedIntermediate(
first0,
first1,
second0,
second1,
differenceRound,
invert).StoreUnsafe(ref maskReference, (nuint)column);
}
}
for (; column < width; column++)
{
int difference = Math.Abs(firstRow[column] - secondRow[column]);
difference = RoundPowerOfTwo(difference, differenceRound);
int alpha = Math.Min(MaximumMaskAlpha, 38 + (difference >> 4));
maskRow[column] = (byte)(invert ? MaximumMaskAlpha - alpha : alpha);
}
}
}
/// <summary>
/// Blends two compound intermediates through a luma-resolution mask.
/// </summary>
public static void BlendIntermediate(
Span<byte> destination,
int destinationStride,
ReadOnlySpan<ushort> first,
int firstStride,
ReadOnlySpan<ushort> second,
int secondStride,
ReadOnlySpan<byte> mask,
int maskStride,
int width,
int height,
int subX,
int subY,
int bitDepth)
{
GetIntermediateRounding(bitDepth, out int roundBits, out int roundOffset);
for (int row = 0; row < height; row++)
{
Span<byte> destinationRow = destination.Slice(row * destinationStride, width);
ReadOnlySpan<ushort> firstRow = first.Slice(row * firstStride, width);
ReadOnlySpan<ushort> secondRow = second.Slice(row * secondStride, width);
ref byte destinationReference = ref MemoryMarshal.GetReference(destinationRow);
ref ushort firstReference = ref MemoryMarshal.GetReference(firstRow);
ref ushort secondReference = ref MemoryMarshal.GetReference(secondRow);
int column = 0;
if (Vector128.IsHardwareAccelerated && subX == 0 && subY == 0)
{
ref byte maskReference = ref MemoryMarshal.GetReference(mask);
int maskRowOffset = row * maskStride;
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));
Vector128<byte> alpha = Vector128.LoadUnsafe(
ref maskReference,
(nuint)(maskRowOffset + column));
BlendIntermediate(
first0,
first1,
second0,
second1,
alpha,
roundBits,
roundOffset).StoreUnsafe(ref destinationReference, (nuint)column);
}
}
for (; column < width; column++)
{
int alpha = GetSubsampledMaskAlpha(mask, maskStride, row, column, subX, subY);
// Mask blending also truncates its Q6 result because final pixel rounding is still pending. Adding
// a half-unit here would produce a second rounding step and diverge from pinned libaom.
int result = ((alpha * firstRow[column]) + ((MaximumMaskAlpha - alpha) * secondRow[column])) >> MaskWeightBits;
result -= roundOffset;
destinationRow[column] = (byte)Math.Clamp(RoundPowerOfTwo(result, roundBits), 0, byte.MaxValue);
}
}
}
/// <summary>
/// Equal-averages sixteen compound lanes and converts them to final 8-bit samples.
/// </summary>
private static Vector128<byte> AverageIntermediate(
Vector128<ushort> first0,
Vector128<ushort> first1,
Vector128<ushort> second0,
Vector128<ushort> second1,
int roundBits,
int roundOffset)
=> Vector128.Narrow(
FinalizeIntermediate(
(first0 & second0) + ((first0 ^ second0) >> 1),
roundBits,
roundOffset),
FinalizeIntermediate(
(first1 & second1) + ((first1 ^ second1) >> 1),
roundBits,
roundOffset));
/// <summary>
/// Distance-weights sixteen compound lanes and converts them to final 8-bit samples.
/// </summary>
private static Vector128<byte> DistanceWeightedIntermediate(
Vector128<ushort> first0,
Vector128<ushort> first1,
Vector128<ushort> second0,
Vector128<ushort> second1,
int firstWeight,
int secondWeight,
int roundBits,
int roundOffset)
=> Vector128.Narrow(
DistanceWeightedIntermediate(first0, second0, firstWeight, secondWeight, roundBits, roundOffset),
DistanceWeightedIntermediate(first1, second1, firstWeight, secondWeight, roundBits, roundOffset));
/// <summary>
/// Distance-weights eight compound lanes without overflowing the unsigned intermediate range.
/// </summary>
private static Vector128<ushort> DistanceWeightedIntermediate(
Vector128<ushort> first,
Vector128<ushort> second,
int firstWeight,
int secondWeight,
int roundBits,
int roundOffset)
{
Vector128<int> firstLower = Vector128.WidenLower(first).AsInt32();
Vector128<int> firstUpper = Vector128.WidenUpper(first).AsInt32();
Vector128<int> secondLower = Vector128.WidenLower(second).AsInt32();
Vector128<int> secondUpper = Vector128.WidenUpper(second).AsInt32();
Vector128<int> lower =
((firstLower * firstWeight) + (secondLower * secondWeight)) >> DistanceWeightBits;
Vector128<int> upper =
((firstUpper * firstWeight) + (secondUpper * secondWeight)) >> DistanceWeightBits;
return Vector128.Narrow(
FinalizeIntermediate(lower, roundBits, roundOffset),
FinalizeIntermediate(upper, roundBits, roundOffset)).AsUInt16();
}
/// <summary>
/// Creates sixteen difference-weighted mask values from compound intermediates.
/// </summary>
private static Vector128<byte> DifferenceWeightedIntermediate(
Vector128<ushort> first0,
Vector128<ushort> first1,
Vector128<ushort> second0,
Vector128<ushort> second1,
int differenceRound,
bool invert)
=> Vector128.Narrow(
DifferenceWeightedIntermediate(first0, second0, differenceRound, invert),
DifferenceWeightedIntermediate(first1, second1, differenceRound, invert));
/// <summary>
/// Creates eight difference-weighted mask values without losing the required pre-alpha rounding.
/// </summary>
private static Vector128<ushort> DifferenceWeightedIntermediate(
Vector128<ushort> first,
Vector128<ushort> second,
int differenceRound,
bool invert)
{
Vector128<ushort> difference = Vector128.Max(first, second) - Vector128.Min(first, second);
Vector128<int> lower = DifferenceWeightedIntermediate(
Vector128.WidenLower(difference).AsInt32(),
differenceRound,
invert);
Vector128<int> upper = DifferenceWeightedIntermediate(
Vector128.WidenUpper(difference).AsInt32(),
differenceRound,
invert);
return Vector128.Narrow(lower, upper).AsUInt16();
}
/// <summary>
/// Converts four intermediate differences to the decoded type-38 mask range.
/// </summary>
private static Vector128<int> DifferenceWeightedIntermediate(
Vector128<int> difference,
int differenceRound,
bool invert)
{
if (differenceRound != 0)
{
difference = (difference + Vector128.Create(1 << (differenceRound - 1))) >> differenceRound;
}
Vector128<int> maximum = Vector128.Create(MaximumMaskAlpha);
Vector128<int> alpha = Vector128.Min(maximum, (difference >> 4) + Vector128.Create(38));
return invert ? maximum - alpha : alpha;
}
/// <summary>
/// Mask-blends sixteen compound lanes and converts them to final 8-bit samples.
/// </summary>
private static Vector128<byte> BlendIntermediate(
Vector128<ushort> first0,
Vector128<ushort> first1,
Vector128<ushort> second0,
Vector128<ushort> second1,
Vector128<byte> alpha,
int roundBits,
int roundOffset)
=> Vector128.Narrow(
BlendIntermediate(
first0,
second0,
Vector128.WidenLower(alpha),
roundBits,
roundOffset),
BlendIntermediate(
first1,
second1,
Vector128.WidenUpper(alpha),
roundBits,
roundOffset));
/// <summary>
/// Mask-blends eight compound lanes after widening every product to signed 32-bit precision.
/// </summary>
private static Vector128<ushort> BlendIntermediate(
Vector128<ushort> first,
Vector128<ushort> second,
Vector128<ushort> alpha,
int roundBits,
int roundOffset)
{
Vector128<int> firstLower = Vector128.WidenLower(first).AsInt32();
Vector128<int> firstUpper = Vector128.WidenUpper(first).AsInt32();
Vector128<int> secondLower = Vector128.WidenLower(second).AsInt32();
Vector128<int> secondUpper = Vector128.WidenUpper(second).AsInt32();
Vector128<int> alphaLower = Vector128.WidenLower(alpha).AsInt32();
Vector128<int> alphaUpper = Vector128.WidenUpper(alpha).AsInt32();
Vector128<int> maximum = Vector128.Create(MaximumMaskAlpha);
Vector128<int> lower =
((alphaLower * firstLower) + ((maximum - alphaLower) * secondLower)) >> MaskWeightBits;
Vector128<int> upper =
((alphaUpper * firstUpper) + ((maximum - alphaUpper) * secondUpper)) >> MaskWeightBits;
return Vector128.Narrow(
FinalizeIntermediate(lower, roundBits, roundOffset),
FinalizeIntermediate(upper, roundBits, roundOffset)).AsUInt16();
}
/// <summary>
/// Removes the compound bias and final fractional precision from eight unsigned lanes.
/// </summary>
private static Vector128<ushort> FinalizeIntermediate(
Vector128<ushort> value,
int roundBits,
int roundOffset)
{
Vector128<short> result = (value - Vector128.Create((ushort)roundOffset)).AsInt16();
if (roundBits != 0)
{
result = (result + Vector128.Create((short)(1 << (roundBits - 1)))) >> roundBits;
}
result = Vector128.Max(Vector128<short>.Zero, Vector128.Min(Vector128.Create((short)byte.MaxValue), result));
return result.AsUInt16();
}
/// <summary>
/// Removes the compound bias and final fractional precision from four widened lanes.
/// </summary>
private static Vector128<int> FinalizeIntermediate(
Vector128<int> value,
int roundBits,
int roundOffset)
{
Vector128<int> result = value - Vector128.Create(roundOffset);
if (roundBits != 0)
{
result = (result + Vector128.Create(1 << (roundBits - 1))) >> roundBits;
}
return Vector128.Max(Vector128<int>.Zero, Vector128.Min(Vector128.Create((int)byte.MaxValue), result));
}
/// <summary>
/// Gets the mask alpha for one plane sample, averaging its two or four luma samples when required.
/// </summary>
private static int GetSubsampledMaskAlpha(
ReadOnlySpan<byte> mask,
int maskStride,
int row,
int column,
int subX,
int subY)
{
int maskRow = row << subY;
int maskColumn = column << subX;
int alpha = mask[(maskRow * maskStride) + maskColumn];
if (subX != 0)
{
alpha += mask[(maskRow * maskStride) + maskColumn + 1];
}
if (subY != 0)
{
int lowerOffset = ((maskRow + 1) * maskStride) + maskColumn;
alpha += mask[lowerOffset];
if (subX != 0)
{
alpha += mask[lowerOffset + 1];
}
}
int sampleCountShift = subX + subY;
return sampleCountShift == 0
? alpha
: RoundPowerOfTwo(alpha, sampleCountShift);
}
/// <summary>
/// Derives the bias and remaining fractional precision of a compound intermediate.
/// </summary>
private static void GetIntermediateRounding(int bitDepth, out int roundBits, out int roundOffset)
{
int intermediateRange = bitDepth + 7 - 3 + 2;
int round0 = 3 + Math.Max(intermediateRange - 16, 0);
int offsetBits = bitDepth + 14 - round0;
roundBits = 14 - round0 - Av1InterPredictor.CompoundRound1Bits;
roundOffset = (1 << (offsetBits - Av1InterPredictor.CompoundRound1Bits)) +
(1 << (offsetBits - Av1InterPredictor.CompoundRound1Bits - 1));
}
/// <summary>
/// Applies AV1's positive power-of-two rounding rule.
/// </summary>
private static int RoundPowerOfTwo(int value, int bits)
=> bits == 0 ? value : (value + (1 << (bits - 1))) >> bits;
}

732
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Compound.cs

@ -0,0 +1,732 @@
// 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>
/// Produces the biased high-precision intermediates required by compound inter prediction.
/// </content>
internal static partial class Av1InterPredictor
{
/// <summary>
/// The second-round shift retained by every compound convolution path.
/// </summary>
internal const int CompoundRound1Bits = 7;
/// <summary>
/// Reconstructs one 8-bit translational reference into AV1's unsigned compound intermediate format.
/// </summary>
public static void PredictCompound(
ReadOnlySpan<byte> source,
int sourceStride,
int sourceOrigin,
Span<ushort> destination,
int destinationStride,
int width,
int height,
Av1InterpolationFilter horizontalFilter,
Av1InterpolationFilter verticalFilter,
int horizontalPhase,
int verticalPhase,
Span<short> scratch)
{
if (Vector128.IsHardwareAccelerated)
{
PredictCompoundVector128(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalFilter,
verticalFilter,
horizontalPhase,
verticalPhase,
scratch);
return;
}
PredictCompoundScalar(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalFilter,
verticalFilter,
horizontalPhase,
verticalPhase,
scratch);
}
/// <summary>
/// Reconstructs one compound intermediate without explicit hardware intrinsics.
/// </summary>
public static void PredictCompoundScalar(
ReadOnlySpan<byte> source,
int sourceStride,
int sourceOrigin,
Span<ushort> destination,
int destinationStride,
int width,
int height,
Av1InterpolationFilter horizontalFilter,
Av1InterpolationFilter verticalFilter,
int horizontalPhase,
int verticalPhase,
Span<short> scratch)
{
ReadOnlySpan<short> horizontalCoefficients = GetCompoundCoefficients(horizontalFilter, horizontalPhase, width <= 4);
ReadOnlySpan<short> verticalCoefficients = GetCompoundCoefficients(verticalFilter, verticalPhase, height <= 4);
int roundBits = (2 * FilterBits) - Round0Bits - CompoundRound1Bits;
int offsetBits = 8 + (2 * FilterBits) - Round0Bits;
int roundOffset = (1 << (offsetBits - CompoundRound1Bits)) +
(1 << (offsetBits - CompoundRound1Bits - 1));
ref byte sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
if (horizontalPhase == 0 && verticalPhase == 0)
{
for (int row = 0; row < height; row++)
{
ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride);
ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride);
for (int column = 0; column < width; column++)
{
Unsafe.Add(ref destinationRow, column) =
(ushort)((Unsafe.Add(ref sourceRow, column) << roundBits) + roundOffset);
}
}
return;
}
if (verticalPhase == 0)
{
GetEffectiveKernel(horizontalCoefficients, out int firstCoefficient, out int tapCount);
ref short coefficientBase = ref Unsafe.Add(
ref MemoryMarshal.GetReference(horizontalCoefficients),
firstCoefficient);
int sourceOffset = firstCoefficient - 3;
for (int row = 0; row < height; row++)
{
ref byte sourceRow = ref Unsafe.Add(ref sourceBase, (row * sourceStride) + sourceOffset);
ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride);
for (int column = 0; column < width; column++)
{
int sum = ConvolveScalar(ref Unsafe.Add(ref sourceRow, column), 1, ref coefficientBase, tapCount);
int result = (RoundPowerOfTwo(sum, Round0Bits) << (FilterBits - CompoundRound1Bits)) + roundOffset;
Unsafe.Add(ref destinationRow, column) = (ushort)result;
}
}
return;
}
if (horizontalPhase == 0)
{
GetEffectiveKernel(verticalCoefficients, out int firstCoefficient, out int tapCount);
ref short coefficientBase = ref Unsafe.Add(
ref MemoryMarshal.GetReference(verticalCoefficients),
firstCoefficient);
int sourceOffset = firstCoefficient - 3;
int firstPassBits = FilterBits - Round0Bits;
for (int row = 0; row < height; row++)
{
ref byte sourceRow = ref Unsafe.Add(ref sourceBase, (row + sourceOffset) * sourceStride);
ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride);
for (int column = 0; column < width; column++)
{
int sum = ConvolveScalar(
ref Unsafe.Add(ref sourceRow, column),
sourceStride,
ref coefficientBase,
tapCount);
int result = RoundPowerOfTwo(sum << firstPassBits, CompoundRound1Bits) + roundOffset;
Unsafe.Add(ref destinationRow, column) = (ushort)result;
}
}
return;
}
GetEffectiveKernel(horizontalCoefficients, out int firstHorizontalCoefficient, out int horizontalTapCount);
GetEffectiveKernel(verticalCoefficients, out int firstVerticalCoefficient, out int verticalTapCount);
ref short horizontalCoefficientBase = ref Unsafe.Add(
ref MemoryMarshal.GetReference(horizontalCoefficients),
firstHorizontalCoefficient);
ref short verticalCoefficientBase = ref Unsafe.Add(
ref MemoryMarshal.GetReference(verticalCoefficients),
firstVerticalCoefficient);
int scratchStride = Math.Max(width, MinimumScratchStride);
int intermediateHeight = height + verticalTapCount - 1;
int horizontalSourceOffset = firstHorizontalCoefficient - 3;
int verticalSourceOffset = firstVerticalCoefficient - 3;
int horizontalBias = 1 << (8 + FilterBits - 1);
ref short scratchBase = ref MemoryMarshal.GetReference(scratch);
// The Q7 horizontal pass keeps enough precision for the vertical pass while the positive bias makes every
// intermediate representable by signed 16-bit scratch. This is the same no-round compound shape as libaom.
for (int row = 0; row < intermediateHeight; row++)
{
ref byte sourceRow = ref Unsafe.Add(
ref sourceBase,
((row + verticalSourceOffset) * sourceStride) + horizontalSourceOffset);
ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride);
for (int column = 0; column < width; column++)
{
int sum = horizontalBias + ConvolveScalar(
ref Unsafe.Add(ref sourceRow, column),
1,
ref horizontalCoefficientBase,
horizontalTapCount);
Unsafe.Add(ref scratchRow, column) = (short)RoundPowerOfTwo(sum, Round0Bits);
}
}
int verticalBias = 1 << offsetBits;
for (int row = 0; row < height; row++)
{
ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride);
ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride);
for (int column = 0; column < width; column++)
{
int sum = verticalBias + ConvolveScalar(
ref Unsafe.Add(ref scratchRow, column),
scratchStride,
ref verticalCoefficientBase,
verticalTapCount);
Unsafe.Add(ref destinationRow, column) = (ushort)RoundPowerOfTwo(sum, CompoundRound1Bits);
}
}
}
/// <summary>
/// Reconstructs one compound intermediate through the 128-bit convolution tier.
/// </summary>
private static void PredictCompoundVector128(
ReadOnlySpan<byte> source,
int sourceStride,
int sourceOrigin,
Span<ushort> destination,
int destinationStride,
int width,
int height,
Av1InterpolationFilter horizontalFilter,
Av1InterpolationFilter verticalFilter,
int horizontalPhase,
int verticalPhase,
Span<short> scratch)
{
ReadOnlySpan<short> horizontalCoefficients = GetCompoundCoefficients(horizontalFilter, horizontalPhase, width <= 4);
ReadOnlySpan<short> verticalCoefficients = GetCompoundCoefficients(verticalFilter, verticalPhase, height <= 4);
int roundBits = (2 * FilterBits) - Round0Bits - CompoundRound1Bits;
int offsetBits = 8 + (2 * FilterBits) - Round0Bits;
int roundOffset = (1 << (offsetBits - CompoundRound1Bits)) +
(1 << (offsetBits - CompoundRound1Bits - 1));
if (horizontalPhase == 0 && verticalPhase == 0)
{
CopyCompoundVector128(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
roundBits,
roundOffset);
return;
}
if (verticalPhase == 0)
{
GetEffectiveKernel(horizontalCoefficients, out int firstCoefficient, out int tapCount);
FilterCompoundDirectVector128(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalCoefficients[firstCoefficient..],
tapCount,
firstCoefficient - 3,
tapStride: 1,
preShift: 0,
round: Round0Bits,
roundOffset);
return;
}
if (horizontalPhase == 0)
{
GetEffectiveKernel(verticalCoefficients, out int firstCoefficient, out int tapCount);
FilterCompoundDirectVector128(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
verticalCoefficients[firstCoefficient..],
tapCount,
(firstCoefficient - 3) * sourceStride,
sourceStride,
FilterBits - Round0Bits,
CompoundRound1Bits,
roundOffset);
return;
}
GetEffectiveKernel(horizontalCoefficients, out int firstHorizontalCoefficient, out int horizontalTapCount);
GetEffectiveKernel(verticalCoefficients, out int firstVerticalCoefficient, out int verticalTapCount);
FilterCompound2DVector128(
source,
sourceStride,
sourceOrigin,
destination,
destinationStride,
width,
height,
horizontalCoefficients[firstHorizontalCoefficient..],
horizontalTapCount,
firstHorizontalCoefficient - 3,
verticalCoefficients[firstVerticalCoefficient..],
verticalTapCount,
firstVerticalCoefficient - 3,
scratch);
}
/// <summary>
/// Copies integer-position samples into biased compound intermediates in sixteen-sample groups.
/// </summary>
private static void CopyCompoundVector128(
ReadOnlySpan<byte> source,
int sourceStride,
int sourceOrigin,
Span<ushort> destination,
int destinationStride,
int width,
int height,
int roundBits,
int roundOffset)
{
ref byte sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
Vector128<ushort> offset = Vector128.Create((ushort)roundOffset);
for (int row = 0; row < height; row++)
{
ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride);
ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride);
int column = 0;
int vectorEnd = width - Vector128<byte>.Count;
for (; column <= vectorEnd; column += Vector128<byte>.Count)
{
Vector128<byte> samples = Vector128.LoadUnsafe(ref sourceRow, (nuint)column);
((Vector128.WidenLower(samples) << roundBits) + offset).StoreUnsafe(
ref destinationRow,
(nuint)column);
((Vector128.WidenUpper(samples) << roundBits) + offset).StoreUnsafe(
ref destinationRow,
(nuint)(column + Vector128<ushort>.Count));
}
if (column == 0)
{
// Narrow AV1 blocks still use the SIMD load; the width-specific stores preserve the adjacent block.
Vector128<byte> samples = Vector128.LoadUnsafe(ref sourceRow);
StoreCompoundVectors(
(Vector128.WidenLower(samples) << roundBits) + offset,
(Vector128.WidenUpper(samples) << roundBits) + offset,
ref destinationRow,
width);
continue;
}
for (; column < width; column++)
{
Unsafe.Add(ref destinationRow, column) =
(ushort)((Unsafe.Add(ref sourceRow, column) << roundBits) + roundOffset);
}
}
}
/// <summary>
/// Applies one compound convolution direction in sixteen-sample groups.
/// </summary>
private static void FilterCompoundDirectVector128(
ReadOnlySpan<byte> source,
int sourceStride,
int sourceOrigin,
Span<ushort> destination,
int destinationStride,
int width,
int height,
ReadOnlySpan<short> coefficients,
int tapCount,
int sourceOffset,
int tapStride,
int preShift,
int round,
int roundOffset)
{
ref byte sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
ref short coefficientBase = ref MemoryMarshal.GetReference(coefficients);
Vector128<int> offset = Vector128.Create(roundOffset);
for (int row = 0; row < height; row++)
{
ref byte sourceRow = ref Unsafe.Add(ref sourceBase, (row * sourceStride) + sourceOffset);
ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride);
int column = 0;
int vectorEnd = width - Vector128<byte>.Count;
for (; column <= vectorEnd; column += Vector128<byte>.Count)
{
Convolve(
ref sourceRow,
tapStride,
(nuint)column,
ref coefficientBase,
tapCount,
Vector128<int>.Zero,
out Vector128<int> result0,
out Vector128<int> result1,
out Vector128<int> result2,
out Vector128<int> result3);
PrepareCompoundResults(
ref result0,
ref result1,
ref result2,
ref result3,
preShift,
round,
offset);
StoreCompoundVectors(result0, result1, result2, result3, ref destinationRow, column, Vector128<byte>.Count);
}
if (column == 0)
{
Convolve(
ref sourceRow,
tapStride,
0,
ref coefficientBase,
tapCount,
Vector128<int>.Zero,
out Vector128<int> result0,
out Vector128<int> result1,
out Vector128<int> result2,
out Vector128<int> result3);
PrepareCompoundResults(
ref result0,
ref result1,
ref result2,
ref result3,
preShift,
round,
offset);
StoreCompoundVectors(result0, result1, result2, result3, ref destinationRow, 0, width);
continue;
}
for (; column < width; column++)
{
int sum = ConvolveScalar(
ref Unsafe.Add(ref sourceRow, column),
tapStride,
ref coefficientBase,
tapCount);
sum = RoundPowerOfTwo(sum << preShift, round) + roundOffset;
Unsafe.Add(ref destinationRow, column) = (ushort)sum;
}
}
}
/// <summary>
/// Applies separable compound convolution through caller-owned signed scratch.
/// </summary>
private static void FilterCompound2DVector128(
ReadOnlySpan<byte> source,
int sourceStride,
int sourceOrigin,
Span<ushort> destination,
int destinationStride,
int width,
int height,
ReadOnlySpan<short> horizontalCoefficients,
int horizontalTapCount,
int horizontalSourceOffset,
ReadOnlySpan<short> verticalCoefficients,
int verticalTapCount,
int verticalSourceOffset,
Span<short> scratch)
{
ref byte sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
ref short scratchBase = ref MemoryMarshal.GetReference(scratch);
ref short horizontalCoefficientBase = ref MemoryMarshal.GetReference(horizontalCoefficients);
ref short verticalCoefficientBase = ref MemoryMarshal.GetReference(verticalCoefficients);
int scratchStride = Math.Max(width, MinimumScratchStride);
int intermediateHeight = height + verticalTapCount - 1;
Vector128<int> horizontalBias = Vector128.Create(1 << (8 + FilterBits - 1));
// The complete narrow-block vector is retained in scratch because the vertical pass consumes the same lanes.
// Wider blocks use one vector per sixteen output samples and finish any nonstandard tail scalarly.
for (int row = 0; row < intermediateHeight; row++)
{
ref byte sourceRow = ref Unsafe.Add(
ref sourceBase,
((row + verticalSourceOffset) * sourceStride) + horizontalSourceOffset);
ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride);
int column = 0;
int vectorEnd = width - Vector128<byte>.Count;
for (; column <= vectorEnd; column += Vector128<byte>.Count)
{
Convolve(
ref sourceRow,
1,
(nuint)column,
ref horizontalCoefficientBase,
horizontalTapCount,
horizontalBias,
out Vector128<int> result0,
out Vector128<int> result1,
out Vector128<int> result2,
out Vector128<int> result3);
Av1IntraPredictorBase.Narrow(
RoundPowerOfTwo(result0, Round0Bits),
RoundPowerOfTwo(result1, Round0Bits)).StoreUnsafe(ref scratchRow, (nuint)column);
Av1IntraPredictorBase.Narrow(
RoundPowerOfTwo(result2, Round0Bits),
RoundPowerOfTwo(result3, Round0Bits)).StoreUnsafe(
ref scratchRow,
(nuint)(column + Vector128<short>.Count));
}
if (column == 0)
{
Convolve(
ref sourceRow,
1,
0,
ref horizontalCoefficientBase,
horizontalTapCount,
horizontalBias,
out Vector128<int> result0,
out Vector128<int> result1,
out Vector128<int> result2,
out Vector128<int> result3);
Av1IntraPredictorBase.Narrow(
RoundPowerOfTwo(result0, Round0Bits),
RoundPowerOfTwo(result1, Round0Bits)).StoreUnsafe(ref scratchRow);
Av1IntraPredictorBase.Narrow(
RoundPowerOfTwo(result2, Round0Bits),
RoundPowerOfTwo(result3, Round0Bits)).StoreUnsafe(
ref scratchRow,
(nuint)Vector128<short>.Count);
continue;
}
for (; column < width; column++)
{
int sum = (1 << (8 + FilterBits - 1)) + ConvolveScalar(
ref Unsafe.Add(ref sourceRow, column),
1,
ref horizontalCoefficientBase,
horizontalTapCount);
Unsafe.Add(ref scratchRow, column) = (short)RoundPowerOfTwo(sum, Round0Bits);
}
}
Vector128<int> verticalBias = Vector128.Create(1 << (8 + (2 * FilterBits) - Round0Bits));
for (int row = 0; row < height; row++)
{
ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride);
ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride);
int column = 0;
int vectorEnd = width - Vector128<byte>.Count;
for (; column <= vectorEnd; column += Vector128<byte>.Count)
{
Convolve(
ref scratchRow,
scratchStride,
(nuint)column,
ref verticalCoefficientBase,
verticalTapCount,
verticalBias,
out Vector128<int> result0,
out Vector128<int> result1);
Convolve(
ref scratchRow,
scratchStride,
(nuint)(column + Vector128<short>.Count),
ref verticalCoefficientBase,
verticalTapCount,
verticalBias,
out Vector128<int> result2,
out Vector128<int> result3);
result0 = RoundPowerOfTwo(result0, CompoundRound1Bits);
result1 = RoundPowerOfTwo(result1, CompoundRound1Bits);
result2 = RoundPowerOfTwo(result2, CompoundRound1Bits);
result3 = RoundPowerOfTwo(result3, CompoundRound1Bits);
StoreCompoundVectors(result0, result1, result2, result3, ref destinationRow, column, Vector128<byte>.Count);
}
if (column == 0)
{
Convolve(
ref scratchRow,
scratchStride,
0,
ref verticalCoefficientBase,
verticalTapCount,
verticalBias,
out Vector128<int> result0,
out Vector128<int> result1);
Convolve(
ref Unsafe.Add(ref scratchRow, Vector128<short>.Count),
scratchStride,
0,
ref verticalCoefficientBase,
verticalTapCount,
verticalBias,
out Vector128<int> result2,
out Vector128<int> result3);
result0 = RoundPowerOfTwo(result0, CompoundRound1Bits);
result1 = RoundPowerOfTwo(result1, CompoundRound1Bits);
result2 = RoundPowerOfTwo(result2, CompoundRound1Bits);
result3 = RoundPowerOfTwo(result3, CompoundRound1Bits);
StoreCompoundVectors(result0, result1, result2, result3, ref destinationRow, 0, width);
continue;
}
for (; column < width; column++)
{
int sum = (1 << (8 + (2 * FilterBits) - Round0Bits)) + ConvolveScalar(
ref Unsafe.Add(ref scratchRow, column),
scratchStride,
ref verticalCoefficientBase,
verticalTapCount);
Unsafe.Add(ref destinationRow, column) =
(ushort)RoundPowerOfTwo(sum, CompoundRound1Bits);
}
}
}
/// <summary>
/// Applies the compound direct-filter shifts and bias to sixteen convolution results.
/// </summary>
private static void PrepareCompoundResults(
ref Vector128<int> result0,
ref Vector128<int> result1,
ref Vector128<int> result2,
ref Vector128<int> result3,
int preShift,
int round,
Vector128<int> offset)
{
result0 = RoundPowerOfTwo(result0 << preShift, round) + offset;
result1 = RoundPowerOfTwo(result1 << preShift, round) + offset;
result2 = RoundPowerOfTwo(result2 << preShift, round) + offset;
result3 = RoundPowerOfTwo(result3 << preShift, round) + offset;
}
/// <summary>
/// Packs and stores up to sixteen unsigned compound results.
/// </summary>
private static void StoreCompoundVectors(
Vector128<int> result0,
Vector128<int> result1,
Vector128<int> result2,
Vector128<int> result3,
ref ushort destination,
int destinationOffset,
int width)
{
Vector128<ushort> lower = Av1IntraPredictorBase.Narrow(result0, result1).AsUInt16();
Vector128<ushort> upper = Av1IntraPredictorBase.Narrow(result2, result3).AsUInt16();
ref ushort destinationStart = ref Unsafe.Add(ref destination, destinationOffset);
StoreCompoundVectors(lower, upper, ref destinationStart, width);
}
/// <summary>
/// Stores up to sixteen packed compound results without crossing the logical block edge.
/// </summary>
private static void StoreCompoundVectors(
Vector128<ushort> lower,
Vector128<ushort> upper,
ref ushort destination,
int width)
{
int lowerWidth = Math.Min(width, Vector128<ushort>.Count);
StorePartial(lower, ref destination, lowerWidth);
if (width > Vector128<ushort>.Count)
{
StorePartial(
upper,
ref Unsafe.Add(ref destination, Vector128<ushort>.Count),
width - Vector128<ushort>.Count);
}
}
/// <summary>
/// Gets the selected interpolation kernel for compound traversal.
/// </summary>
private static ReadOnlySpan<short> GetCompoundCoefficients(
Av1InterpolationFilter filter,
int phase,
bool useReducedFilter)
=> filter switch
{
Av1InterpolationFilter.Regular => RegularOperator.GetCoefficients(phase, useReducedFilter),
Av1InterpolationFilter.Smooth => SmoothOperator.GetCoefficients(phase, useReducedFilter),
Av1InterpolationFilter.Sharp => SharpOperator.GetCoefficients(phase, useReducedFilter),
_ => BilinearOperator.GetCoefficients(phase, useReducedFilter),
};
}

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

@ -517,8 +517,9 @@ internal static partial class Av1InterPredictor
if (width < Vector128<ushort>.Count) if (width < Vector128<ushort>.Count)
{ {
// Four high-bit-depth samples occupy exactly the lower 64 bits of the vector. // Subsampled sub-8x8 chroma can be two samples wide, so retain the vector load while limiting
Vector128.LoadUnsafe(ref sourceRow).GetLower().StoreUnsafe(ref destinationRow); // the store to the logical row width.
StorePartial(Vector128.LoadUnsafe(ref sourceRow), ref destinationRow, width);
continue; continue;
} }

32
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.OneDimension.cs

@ -226,8 +226,9 @@ internal static partial class Av1InterPredictor
Convolve(ref sourceRow, tapStride, 0, ref coefficientBase, tapCount, initial, out Vector128<int> result0, out Vector128<int> result1); Convolve(ref sourceRow, tapStride, 0, ref coefficientBase, tapCount, initial, out Vector128<int> result0, out Vector128<int> result1);
Round(ref result0, ref result1, firstRound, secondRound); Round(ref result0, ref result1, firstRound, secondRound);
// The only legal AV1 width below eight is four samples, exactly the lower Vector64 half. // Subsampled sub-8x8 chroma can be two samples wide. Keep the full source load for throughput, but
PackHighBitDepth(result0, result1, maximum).GetLower().StoreUnsafe(ref destinationRow); // store only the logical row so the adjacent luma owner's chroma prediction remains intact.
StorePartial(PackHighBitDepth(result0, result1, maximum), ref destinationRow, width);
continue; continue;
} }
@ -439,7 +440,7 @@ internal static partial class Av1InterPredictor
} }
/// <summary> /// <summary>
/// Stores the four- or eight-sample prefix of a sixteen-byte prediction vector. /// Stores the two-, four-, or eight-sample prefix of a sixteen-byte prediction vector.
/// </summary> /// </summary>
private static void StorePartial(Vector128<byte> value, ref byte destination, int width) private static void StorePartial(Vector128<byte> value, ref byte destination, int width)
{ {
@ -447,10 +448,33 @@ internal static partial class Av1InterPredictor
{ {
value.GetLower().StoreUnsafe(ref destination); value.GetLower().StoreUnsafe(ref destination);
} }
else else if (width == 4)
{ {
Unsafe.As<byte, uint>(ref destination) = value.AsUInt32().GetElement(0); Unsafe.As<byte, uint>(ref destination) = value.AsUInt32().GetElement(0);
} }
else
{
Unsafe.As<byte, ushort>(ref destination) = value.AsUInt16().GetElement(0);
}
}
/// <summary>
/// Stores the two-, four-, or eight-sample prefix of an eight-ushort prediction vector.
/// </summary>
private static void StorePartial(Vector128<ushort> value, ref ushort destination, int width)
{
if (width == 8)
{
value.StoreUnsafe(ref destination);
}
else if (width == 4)
{
value.GetLower().StoreUnsafe(ref destination);
}
else
{
Unsafe.As<ushort, uint>(ref destination) = value.AsUInt32().GetElement(0);
}
} }
/// <summary> /// <summary>

9
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.TwoDimensions.UInt16.cs

@ -55,8 +55,8 @@ internal static partial class Av1InterPredictor
if (width < Vector128<ushort>.Count) if (width < Vector128<ushort>.Count)
{ {
// AV1's only legal width below eight is four. The reference plane is padded for the full source load, // Subsampled sub-8x8 chroma can be two samples wide. The reference plane is padded for the full source
// and the minimum scratch stride preserves all eight intermediate lanes needed by the vertical pass. // load, and the minimum scratch stride preserves all eight intermediate lanes for the vertical pass.
Convolve( Convolve(
ref sourceRow, ref sourceRow,
1, 1,
@ -132,8 +132,9 @@ internal static partial class Av1InterPredictor
result0 = RoundPowerOfTwo(result0, round1) - roundOffset; result0 = RoundPowerOfTwo(result0, round1) - roundOffset;
result1 = RoundPowerOfTwo(result1, round1) - roundOffset; result1 = RoundPowerOfTwo(result1, round1) - roundOffset;
// Four high-bit-depth samples occupy exactly the lower half of the packed vector. // Retain the vector convolution for two- and four-sample rows while leaving adjacent destination
PackHighBitDepth(result0, result1, maximum).GetLower().StoreUnsafe(ref destinationRow); // samples owned by the neighboring luma block untouched.
StorePartial(PackHighBitDepth(result0, result1, maximum), ref destinationRow, width);
continue; continue;
} }

170
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Warped.cs

@ -126,6 +126,63 @@ internal static partial class Av1InterPredictor
scratch); scratch);
} }
/// <summary>
/// Reconstructs an 8-bit affine warped reference into AV1's unsigned compound intermediate format.
/// </summary>
public static void PredictWarpedCompound(
ReadOnlySpan<byte> source,
int sourceStride,
Point sourceOrigin,
int sourceWidth,
int sourceHeight,
Span<ushort> destination,
int destinationStride,
Point destinationPosition,
int width,
int height,
int subsamplingX,
int subsamplingY,
Av1GlobalMotionParameters parameters,
Span<short> scratch)
{
if (Vector128.IsHardwareAccelerated)
{
PredictWarpedCompound<WarpedVector128Convolution>(
source,
sourceStride,
sourceOrigin,
sourceWidth,
sourceHeight,
destination,
destinationStride,
destinationPosition,
width,
height,
subsamplingX,
subsamplingY,
parameters,
scratch);
return;
}
PredictWarpedCompound<WarpedScalarConvolution>(
source,
sourceStride,
sourceOrigin,
sourceWidth,
sourceHeight,
destination,
destinationStride,
destinationPosition,
width,
height,
subsamplingX,
subsamplingY,
parameters,
scratch);
}
/// <summary> /// <summary>
/// Reconstructs a high-bit-depth affine warped prediction using the widest supported convolution operator. /// Reconstructs a high-bit-depth affine warped prediction using the widest supported convolution operator.
/// </summary> /// </summary>
@ -220,6 +277,40 @@ internal static partial class Av1InterPredictor
parameters, parameters,
scratch); scratch);
/// <summary>
/// Reconstructs an 8-bit affine warped reference into compound intermediates without explicit hardware intrinsics.
/// </summary>
public static void PredictWarpedCompoundScalar(
ReadOnlySpan<byte> source,
int sourceStride,
Point sourceOrigin,
int sourceWidth,
int sourceHeight,
Span<ushort> destination,
int destinationStride,
Point destinationPosition,
int width,
int height,
int subsamplingX,
int subsamplingY,
Av1GlobalMotionParameters parameters,
Span<short> scratch)
=> PredictWarpedCompound<WarpedScalarConvolution>(
source,
sourceStride,
sourceOrigin,
sourceWidth,
sourceHeight,
destination,
destinationStride,
destinationPosition,
width,
height,
subsamplingX,
subsamplingY,
parameters,
scratch);
/// <summary> /// <summary>
/// Reconstructs a high-bit-depth affine warped prediction without explicit hardware intrinsics. /// Reconstructs a high-bit-depth affine warped prediction without explicit hardware intrinsics.
/// </summary> /// </summary>
@ -335,6 +426,85 @@ internal static partial class Av1InterPredictor
} }
} }
/// <summary>
/// Reconstructs one 8-bit warped reference without discarding the compound convolution precision.
/// </summary>
private static void PredictWarpedCompound<TConvolution>(
ReadOnlySpan<byte> source,
int sourceStride,
Point sourceOrigin,
int sourceWidth,
int sourceHeight,
Span<ushort> destination,
int destinationStride,
Point destinationPosition,
int width,
int height,
int subsamplingX,
int subsamplingY,
Av1GlobalMotionParameters parameters,
Span<short> scratch)
where TConvolution : struct, IWarpedConvolution
{
ref byte sourceBase = ref MemoryMarshal.GetReference(source);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
Span<ushort> intermediate = MemoryMarshal.Cast<short, ushort>(scratch)[..WarpedScratchLength];
int horizontalBias = 1 << (8 + FilterBits - 1);
int verticalBias = 1 << (8 + (2 * FilterBits) - Round0Bits);
for (int tileRow = destinationPosition.Y; tileRow < destinationPosition.Y + height; tileRow += WarpedTileSize)
{
for (int tileColumn = destinationPosition.X; tileColumn < destinationPosition.X + width; tileColumn += WarpedTileSize)
{
DeriveWarpedTilePosition(
parameters,
tileColumn,
tileRow,
subsamplingX,
subsamplingY,
out int integerX,
out int integerY,
out int phaseX,
out int phaseY);
for (int row = -7; row < 8; row++)
{
int sourceY = Math.Clamp(integerY + row, 0, sourceHeight - 1);
int phase = phaseX + (parameters.Beta * (row + 4));
for (int column = -4; column < 4; column++)
{
int sourceX = integerX + column - 3;
int sourceIndex = ((sourceOrigin.Y + sourceY) * sourceStride) + sourceOrigin.X + sourceX;
ref short coefficients = ref GetWarpedFilterReference(phase);
int sum = horizontalBias + TConvolution.Convolve(ref Unsafe.Add(ref sourceBase, sourceIndex), ref coefficients);
intermediate[((row + 7) * WarpedTileSize) + column + 4] =
(ushort)RoundPowerOfTwoScalar(sum, Round0Bits);
phase += parameters.Alpha;
}
}
int tileHeight = Math.Min(WarpedTileSize, destinationPosition.Y + height - tileRow);
int tileWidth = Math.Min(WarpedTileSize, destinationPosition.X + width - tileColumn);
for (int row = 0; row < tileHeight; row++)
{
int phase = phaseY + (parameters.Delta * row);
int destinationRowOffset = (tileRow - destinationPosition.Y + row) * destinationStride;
for (int column = 0; column < tileWidth; column++)
{
ref ushort intermediateSource = ref intermediate[(row * WarpedTileSize) + column];
ref short coefficients = ref GetWarpedFilterReference(phase);
int sum = verticalBias + TConvolution.ConvolveVertical(ref intermediateSource, WarpedTileSize, ref coefficients);
Unsafe.Add(ref destinationBase, destinationRowOffset + tileColumn - destinationPosition.X + column) =
(ushort)RoundPowerOfTwoScalar(sum, CompoundRound1Bits);
phase += parameters.Gamma;
}
}
}
}
}
/// <summary> /// <summary>
/// Reconstructs one high-bit-depth warped block through a closed convolution operator. /// Reconstructs one high-bit-depth warped block through a closed convolution operator.
/// </summary> /// </summary>

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

@ -826,7 +826,9 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <remarks>Corresponds to <c>reset_skip_context</c> in SVT-AV1.</remarks> /// <remarks>Corresponds to <c>reset_skip_context</c> in SVT-AV1.</remarks>
private void ResetSkipContext(ref Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo) private void ResetSkipContext(ref Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo)
{ {
int planesCount = this.SequenceHeader.ColorConfig.PlaneCount; // Subsampled 4x4 luma blocks can share chroma ownership with an adjacent luma block. A skipped block that is
// not the chroma reference must preserve those shared coefficient contexts for the owning block.
int planesCount = partitionInfo.IsChroma ? this.SequenceHeader.ColorConfig.PlaneCount : 1;
for (int i = 0; i < planesCount; i++) for (int i = 0; i < planesCount; i++)
{ {
int subX = (i > 0 && this.SequenceHeader.ColorConfig.SubSamplingX) ? 1 : 0; int subX = (i > 0 && this.SequenceHeader.ColorConfig.SubSamplingX) ? 1 : 0;

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

@ -133,9 +133,11 @@ internal sealed class Av1BlockDecoder : IDisposable
Av1InterPredictor.GetScratchLength(maximumBlockLength, maximumBlockLength), Av1InterPredictor.GetScratchLength(maximumBlockLength, maximumBlockLength),
Av1InterPredictor.GetMaximumScaledScratchLength(maximumBlockLength, maximumBlockLength))); Av1InterPredictor.GetMaximumScaledScratchLength(maximumBlockLength, maximumBlockLength)));
// Compound prediction retains the complete second predictor until both references have been sampled. int compoundMaskLength = (maximumBlockArea + 1) >> 1;
// Reserve it once beside the convolution workspace so block traversal never rents temporary memory.
int predictionScratchLength = maximumBlockArea + predictorWorkingLength; // Compound prediction retains both high-precision reference planes plus the full-resolution luma mask.
// Keeping all three beside the convolution workspace lets chroma reuse the luma mask without a copy or rent.
int predictionScratchLength = (2 * maximumBlockArea) + compoundMaskLength + predictorWorkingLength;
predictionScratchOwner = this.frameBuffer.MemoryAllocator.Allocate<short>(predictionScratchLength); predictionScratchOwner = this.frameBuffer.MemoryAllocator.Allocate<short>(predictionScratchLength);
this.inverseQuantizationOwner = inverseQuantizationOwner; this.inverseQuantizationOwner = inverseQuantizationOwner;
@ -144,7 +146,7 @@ internal sealed class Av1BlockDecoder : IDisposable
this.predictionDecoder = new( this.predictionDecoder = new(
sequenceHeader, sequenceHeader,
frameHeader, frameHeader,
predictionScratchOwner.Memory[maximumBlockArea..]); predictionScratchOwner.Memory[((2 * maximumBlockArea) + compoundMaskLength)..]);
this.isLoopFilterEnabled = frameHeader.LoopFilterParameters.FilterLevel[0] != 0 || this.isLoopFilterEnabled = frameHeader.LoopFilterParameters.FilterLevel[0] != 0 ||
frameHeader.LoopFilterParameters.FilterLevel[1] != 0; frameHeader.LoopFilterParameters.FilterLevel[1] != 0;
@ -327,13 +329,49 @@ internal sealed class Av1BlockDecoder : IDisposable
int predictionHeight = Math.Max(4, blockSize.GetHeight() >> subY); int predictionHeight = Math.Max(4, blockSize.GetHeight() >> subY);
int maximumBlockLength = 1 << this.sequenceHeader.SuperblockSizeLog2; int maximumBlockLength = 1 << this.sequenceHeader.SuperblockSizeLog2;
int maximumBlockArea = maximumBlockLength * maximumBlockLength; int maximumBlockArea = maximumBlockLength * maximumBlockLength;
int compoundMaskStorageLength = (maximumBlockArea + 1) >> 1;
Span<short> predictionStorage = this.predictionScratchOwner.Memory.Span; Span<short> predictionStorage = this.predictionScratchOwner.Memory.Span;
Span<short> secondPredictionStorage = predictionStorage[..maximumBlockArea]; Span<short> secondPredictionStorage = predictionStorage[..maximumBlockArea];
Span<short> predictionScratch = predictionStorage[maximumBlockArea..]; Span<ushort> firstCompoundPrediction = MemoryMarshal.Cast<short, ushort>(
predictionStorage.Slice(maximumBlockArea, maximumBlockArea));
Span<byte> compoundMask = MemoryMarshal.AsBytes(
predictionStorage.Slice(2 * maximumBlockArea, compoundMaskStorageLength))[..(blockSize.GetWidth() * blockSize.GetHeight())];
Span<short> predictionScratch =
predictionStorage[((2 * maximumBlockArea) + compoundMaskStorageLength)..];
Span<byte> secondPrediction = MemoryMarshal.AsBytes(secondPredictionStorage)[..(predictionWidth * predictionHeight)]; Span<byte> secondPrediction = MemoryMarshal.AsBytes(secondPredictionStorage)[..(predictionWidth * predictionHeight)];
Span<ushort> highBitDepthSecondPrediction = MemoryMarshal.Cast<short, ushort>(secondPredictionStorage)[..(predictionWidth * predictionHeight)]; Span<ushort> highBitDepthSecondPrediction = MemoryMarshal.Cast<short, ushort>(secondPredictionStorage)[..(predictionWidth * predictionHeight)];
Span<byte> compoundMask = MemoryMarshal.AsBytes(predictionScratch)[..(predictionWidth * predictionHeight)]; bool usesSub8x8ChromaPrediction =
int referenceCount = isCompound ? 2 : 1; plane != 0 &&
!isCompound &&
this.TryPredictSub8x8Chroma(
ref partitionInfo,
modeInfoPosition,
blockSize,
plane,
subX,
subY,
pixelPosition,
predictionWidth,
predictionHeight,
blockReconstructionBuffer,
highBitDepthBlockReconstructionBuffer,
reconstructionStride,
predictionScratch);
int referenceCount = usesSub8x8ChromaPrediction ? 0 : isCompound ? 2 : 1;
bool hasScaledCompoundReference = isCompound &&
(referenceFrameBuffer!.Width != this.frameHeader.FrameSize.FrameWidth ||
referenceFrameBuffer.Height != this.frameHeader.FrameSize.FrameHeight ||
secondaryReferenceFrameBuffer!.Width != this.frameHeader.FrameSize.FrameWidth ||
secondaryReferenceFrameBuffer.Height != this.frameHeader.FrameSize.FrameHeight);
// Eight-bit compound prediction uses the normative no-round intermediate path below. Scaled and
// high-bit-depth variants remain on their existing paths until their matching kernels are selected.
bool useCompoundIntermediates =
isCompound && !highBitDepth && !hasScaledCompoundReference;
for (int referenceIndex = 0; referenceIndex < referenceCount; referenceIndex++) for (int referenceIndex = 0; referenceIndex < referenceCount; referenceIndex++)
{ {
@ -342,7 +380,9 @@ internal sealed class Av1BlockDecoder : IDisposable
: secondaryReferenceFrameBuffer!; : secondaryReferenceFrameBuffer!;
Av1MotionVector motionVector = modeInfo.MotionVectors[referenceIndex]; Av1MotionVector motionVector = modeInfo.MotionVectors[referenceIndex];
int destinationStride = referenceIndex == 0 ? reconstructionStride : predictionWidth; int destinationStride = useCompoundIntermediates
? predictionWidth
: referenceIndex == 0 ? reconstructionStride : predictionWidth;
bool isScaledReference = activeReferenceFrameBuffer.Width != this.frameHeader.FrameSize.FrameWidth || bool isScaledReference = activeReferenceFrameBuffer.Width != this.frameHeader.FrameSize.FrameWidth ||
activeReferenceFrameBuffer.Height != this.frameHeader.FrameSize.FrameHeight; activeReferenceFrameBuffer.Height != this.frameHeader.FrameSize.FrameHeight;
@ -430,25 +470,50 @@ internal sealed class Av1BlockDecoder : IDisposable
out int sourceStride, out int sourceStride,
out Point sourceOrigin); out Point sourceOrigin);
Span<byte> destination = referenceIndex == 0 if (useCompoundIntermediates)
? blockReconstructionBuffer[reconstructionStride..] {
: secondPrediction; Span<ushort> destination = referenceIndex == 0
? firstCompoundPrediction
Av1InterPredictor.PredictWarped( : highBitDepthSecondPrediction;
source,
sourceStride, Av1InterPredictor.PredictWarpedCompound(
sourceOrigin, source,
referencePlaneWidth, sourceStride,
referencePlaneHeight, sourceOrigin,
destination, referencePlaneWidth,
destinationStride, referencePlaneHeight,
pixelPosition, destination,
predictionWidth, destinationStride,
predictionHeight, pixelPosition,
subX, predictionWidth,
subY, predictionHeight,
warpedMotionParameters, subX,
predictionScratch); subY,
warpedMotionParameters,
predictionScratch);
}
else
{
Span<byte> destination = referenceIndex == 0
? blockReconstructionBuffer[reconstructionStride..]
: secondPrediction;
Av1InterPredictor.PredictWarped(
source,
sourceStride,
sourceOrigin,
referencePlaneWidth,
referencePlaneHeight,
destination,
destinationStride,
pixelPosition,
predictionWidth,
predictionHeight,
subX,
subY,
warpedMotionParameters,
predictionScratch);
}
} }
continue; continue;
@ -564,29 +629,148 @@ internal sealed class Av1BlockDecoder : IDisposable
int sourceIndex = int sourceIndex =
((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) + sourceOrigin.X + (sourceColumnQ4 >> 4); ((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) + sourceOrigin.X + (sourceColumnQ4 >> 4);
Span<byte> destination = referenceIndex == 0 if (useCompoundIntermediates)
? blockReconstructionBuffer[reconstructionStride..] {
: secondPrediction; Span<ushort> destination = referenceIndex == 0
? firstCompoundPrediction
: highBitDepthSecondPrediction;
Av1InterPredictor.Predict( Av1InterPredictor.PredictCompound(
source, source,
sourceStride, sourceStride,
sourceIndex, sourceIndex,
destination, destination,
destinationStride, predictionWidth,
predictionWidth, predictionWidth,
predictionHeight, predictionHeight,
modeInfo.InterpolationFilters[1], modeInfo.InterpolationFilters[1],
modeInfo.InterpolationFilters[0], modeInfo.InterpolationFilters[0],
horizontalPhase, horizontalPhase,
verticalPhase, verticalPhase,
predictionScratch); predictionScratch);
}
else
{
Span<byte> destination = referenceIndex == 0
? blockReconstructionBuffer[reconstructionStride..]
: secondPrediction;
Av1InterPredictor.Predict(
source,
sourceStride,
sourceIndex,
destination,
destinationStride,
predictionWidth,
predictionHeight,
modeInfo.InterpolationFilters[1],
modeInfo.InterpolationFilters[0],
horizontalPhase,
verticalPhase,
predictionScratch);
}
} }
} }
if (isCompound) if (isCompound)
{ {
if (highBitDepth) if (useCompoundIntermediates)
{
Span<byte> destination = blockReconstructionBuffer[reconstructionStride..];
ReadOnlySpan<ushort> first = firstCompoundPrediction[..(predictionWidth * predictionHeight)];
switch (modeInfo.CompoundType)
{
case Av1CompoundType.Average:
Av1CompoundInterPredictor.AverageIntermediate(
destination,
reconstructionStride,
first,
predictionWidth,
highBitDepthSecondPrediction,
predictionWidth,
predictionWidth,
predictionHeight,
bitDepth: 8);
break;
case Av1CompoundType.DistanceWeighted:
Av1CompoundInterPredictor.DistanceWeightedIntermediate(
destination,
reconstructionStride,
first,
predictionWidth,
highBitDepthSecondPrediction,
predictionWidth,
predictionWidth,
predictionHeight,
firstCompoundWeight,
secondCompoundWeight,
bitDepth: 8);
break;
case Av1CompoundType.Wedge:
Av1WedgeMask.Fill(
compoundMask,
predictionWidth,
blockSize,
modeInfo.CompoundWedgeIndex,
modeInfo.CompoundWedgeSign,
subX,
subY,
invert: false);
Av1CompoundInterPredictor.BlendIntermediate(
destination,
reconstructionStride,
first,
predictionWidth,
highBitDepthSecondPrediction,
predictionWidth,
compoundMask,
predictionWidth,
predictionWidth,
predictionHeight,
subX: 0,
subY: 0,
bitDepth: 8);
break;
default:
int lumaWidth = blockSize.GetWidth();
if (plane == 0)
{
Av1CompoundInterPredictor.FillDifferenceWeightedIntermediateMask(
compoundMask,
lumaWidth,
first,
predictionWidth,
highBitDepthSecondPrediction,
predictionWidth,
predictionWidth,
predictionHeight,
bitDepth: 8,
modeInfo.DifferenceWeightedMaskType);
}
Av1CompoundInterPredictor.BlendIntermediate(
destination,
reconstructionStride,
first,
predictionWidth,
highBitDepthSecondPrediction,
predictionWidth,
compoundMask,
lumaWidth,
predictionWidth,
predictionHeight,
subX,
subY,
bitDepth: 8);
break;
}
}
else if (highBitDepth)
{ {
Span<ushort> destination = MemoryMarshal.Cast<short, ushort>( Span<ushort> destination = MemoryMarshal.Cast<short, ushort>(
highBitDepthBlockReconstructionBuffer[reconstructionStride..]); highBitDepthBlockReconstructionBuffer[reconstructionStride..]);
@ -1073,6 +1257,202 @@ internal sealed class Av1BlockDecoder : IDisposable
} }
} }
/// <summary>
/// Reconstructs a subsampled chroma block assembled from multiple neighboring luma inter blocks.
/// </summary>
private bool TryPredictSub8x8Chroma(
ref Av1PartitionInfo partitionInfo,
Point modeInfoPosition,
Av1BlockSize blockSize,
int plane,
int subX,
int subY,
Point pixelPosition,
int predictionWidth,
int predictionHeight,
Span<byte> blockReconstructionBuffer,
Span<short> highBitDepthBlockReconstructionBuffer,
int reconstructionStride,
Span<short> predictionScratch)
{
bool isSub4X = blockSize.GetWidth() == 4 && subX != 0;
bool isSub4Y = blockSize.GetHeight() == 4 && subY != 0;
if (!isSub4X && !isSub4Y)
{
return false;
}
int rowStart = isSub4Y ? -1 : 0;
int columnStart = isSub4X ? -1 : 0;
// One chroma block can cover two or four independently decoded luma blocks. libaom enters this path only
// when every contributing owner is a conventional inter block; otherwise the current block supplies the
// complete chroma prediction through the ordinary path.
for (int row = rowStart; row <= 0; row++)
{
for (int column = columnStart; column <= 0; column++)
{
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(
new Point(modeInfoPosition.X + column, modeInfoPosition.Y + row));
if (candidate.ReferenceFrames[0] < Av1ReferenceFrameType.Last || candidate.UseIntraBlockCopy)
{
return false;
}
}
}
int subPredictionWidth = blockSize.GetWidth() >> subX;
int subPredictionHeight = blockSize.GetHeight() >> subY;
int modeRow = rowStart;
// Chroma ownership is assigned to the bottom-right luma mode record on each subsampled axis. Consequently
// pixelPosition is already the top-left of this assembled plane block even when its first luma owner is at
// row or column -1. Each subprediction writes directly into its final rectangle without a staging copy.
for (int y = 0; y < predictionHeight; y += subPredictionHeight)
{
int modeColumn = columnStart;
for (int x = 0; x < predictionWidth; x += subPredictionWidth)
{
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(
new Point(modeInfoPosition.X + modeColumn, modeInfoPosition.Y + modeRow));
Av1FrameBuffer<byte> referenceFrameBuffer = this.ResolveReferenceFrame(candidate.ReferenceFrames[0]);
Av1MotionVector motionVector = candidate.MotionVectors[0];
Point subPredictionOrigin = new(pixelPosition.X + x, pixelPosition.Y + y);
int destinationOffset = reconstructionStride + (y * reconstructionStride) + x;
bool isScaledReference =
referenceFrameBuffer.Width != this.frameHeader.FrameSize.FrameWidth ||
referenceFrameBuffer.Height != this.frameHeader.FrameSize.FrameHeight;
if (isScaledReference)
{
Span<byte> scaledDestination = default;
Span<ushort> scaledHighBitDepthDestination = default;
if (this.frameBuffer.BytesPerSample == 2)
{
scaledHighBitDepthDestination = MemoryMarshal.Cast<short, ushort>(
highBitDepthBlockReconstructionBuffer[destinationOffset..]);
}
else
{
scaledDestination = blockReconstructionBuffer[destinationOffset..];
}
this.PredictScaledReference(
referenceFrameBuffer,
motionVector,
plane,
subX,
subY,
subPredictionOrigin,
subPredictionWidth,
subPredictionHeight,
candidate.InterpolationFilters[1],
candidate.InterpolationFilters[0],
scaledDestination,
scaledHighBitDepthDestination,
reconstructionStride,
predictionScratch);
}
else
{
int horizontalMotionQ4 = motionVector.Column << (1 - subX);
int verticalMotionQ4 = motionVector.Row << (1 - subY);
int horizontalExtensionQ4 = (4 + subPredictionWidth) << 4;
int verticalExtensionQ4 = (4 + subPredictionHeight) << 4;
int horizontalEdgeScale = 1 << (1 - subX);
int verticalEdgeScale = 1 << (1 - subY);
// The block-relative UMV edges belong to the current coding block, while each contributing luma
// owner supplies only its motion vector and interpolation filters. This is the same split used by
// libaom's sub-8x8 chroma builder.
horizontalMotionQ4 = Av1Math.Clip3(
(partitionInfo.ModeBlockToLeftEdge * horizontalEdgeScale) - horizontalExtensionQ4,
(partitionInfo.ModeBlockToRightEdge * horizontalEdgeScale) + horizontalExtensionQ4 - 16,
horizontalMotionQ4);
verticalMotionQ4 = Av1Math.Clip3(
(partitionInfo.ModeBlockToTopEdge * verticalEdgeScale) - verticalExtensionQ4,
(partitionInfo.ModeBlockToBottomEdge * verticalEdgeScale) + verticalExtensionQ4 - 16,
verticalMotionQ4);
int sourceColumnQ4 = (subPredictionOrigin.X << 4) + horizontalMotionQ4;
int sourceRowQ4 = (subPredictionOrigin.Y << 4) + verticalMotionQ4;
int horizontalPhase = sourceColumnQ4 & 15;
int verticalPhase = sourceRowQ4 & 15;
if (this.frameBuffer.BytesPerSample == 2)
{
Span<ushort> source = referenceFrameBuffer.GetPaddedPlaneSpan16(
(Av1Plane)plane,
subX,
subY,
out int sourceStride,
out Point sourceOrigin);
int sourceIndex =
((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) +
sourceOrigin.X +
(sourceColumnQ4 >> 4);
Span<ushort> destination = MemoryMarshal.Cast<short, ushort>(
highBitDepthBlockReconstructionBuffer[destinationOffset..]);
Av1InterPredictor.Predict(
source,
sourceStride,
sourceIndex,
destination,
reconstructionStride,
subPredictionWidth,
subPredictionHeight,
candidate.InterpolationFilters[1],
candidate.InterpolationFilters[0],
horizontalPhase,
verticalPhase,
this.frameBuffer.BitDepth.GetBitCount(),
predictionScratch);
}
else
{
Span<byte> source = referenceFrameBuffer.GetPaddedPlaneSpan(
(Av1Plane)plane,
subX,
subY,
out int sourceStride,
out Point sourceOrigin);
int sourceIndex =
((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) +
sourceOrigin.X +
(sourceColumnQ4 >> 4);
Av1InterPredictor.Predict(
source,
sourceStride,
sourceIndex,
blockReconstructionBuffer[destinationOffset..],
reconstructionStride,
subPredictionWidth,
subPredictionHeight,
candidate.InterpolationFilters[1],
candidate.InterpolationFilters[0],
horizontalPhase,
verticalPhase,
predictionScratch);
}
}
modeColumn++;
}
modeRow++;
}
return true;
}
/// <summary> /// <summary>
/// Resolves one canonical retained reference frame. /// Resolves one canonical retained reference frame.
/// </summary> /// </summary>

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

@ -636,6 +636,8 @@ public class Av1CompoundBlockDecoderTests
const int frameSize = 32; const int frameSize = 32;
const int blockOrigin = 8; const int blockOrigin = 8;
const int blockSize = 8; const int blockSize = 8;
const int compoundRoundBits = 4;
const int compoundRoundOffset = (1 << 12) + (1 << 11);
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth, frameSize); ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth, frameSize);
ObuFrameHeader frameHeader = CreateFrameHeader(frameSize); ObuFrameHeader frameHeader = CreateFrameHeader(frameSize);
frameHeader.GetReferenceFrameIndices()[0] = 0; frameHeader.GetReferenceFrameIndices()[0] = 0;
@ -667,8 +669,6 @@ public class Av1CompoundBlockDecoderTests
Av1FrameBuffer<byte> firstReference = referenceFrames.Resolve(0)!.FrameBuffer; Av1FrameBuffer<byte> firstReference = referenceFrames.Resolve(0)!.FrameBuffer;
Av1FrameBuffer<byte> secondReference = referenceFrames.Resolve(1)!.FrameBuffer; Av1FrameBuffer<byte> secondReference = referenceFrames.Resolve(1)!.FrameBuffer;
byte[] firstBytePrediction = new byte[blockSize * blockSize];
byte[] secondBytePrediction = new byte[blockSize * blockSize];
ushort[] firstHighBitDepthPrediction = new ushort[blockSize * blockSize]; ushort[] firstHighBitDepthPrediction = new ushort[blockSize * blockSize];
ushort[] secondHighBitDepthPrediction = new ushort[blockSize * blockSize]; ushort[] secondHighBitDepthPrediction = new ushort[blockSize * blockSize];
short[] firstScratch = new short[Av1InterPredictor.WarpedScratchLength]; short[] firstScratch = new short[Av1InterPredictor.WarpedScratchLength];
@ -690,13 +690,13 @@ public class Av1CompoundBlockDecoderTests
out int secondStride, out int secondStride,
out Point secondOrigin); out Point secondOrigin);
Av1InterPredictor.PredictWarpedScalar( Av1InterPredictor.PredictWarpedCompoundScalar(
firstSource, firstSource,
firstStride, firstStride,
firstOrigin, firstOrigin,
frameSize, frameSize,
frameSize, frameSize,
firstBytePrediction, firstHighBitDepthPrediction,
blockSize, blockSize,
blockPosition, blockPosition,
blockSize, blockSize,
@ -706,13 +706,13 @@ public class Av1CompoundBlockDecoderTests
globalMotionParameters, globalMotionParameters,
firstScratch); firstScratch);
Av1InterPredictor.PredictWarpedScalar( Av1InterPredictor.PredictWarpedCompoundScalar(
secondSource, secondSource,
secondStride, secondStride,
secondOrigin, secondOrigin,
frameSize, frameSize,
frameSize, frameSize,
secondBytePrediction, secondHighBitDepthPrediction,
blockSize, blockSize,
blockPosition, blockPosition,
blockSize, blockSize,
@ -822,8 +822,15 @@ public class Av1CompoundBlockDecoderTests
int predictionIndex = (row * blockSize) + column; int predictionIndex = (row * blockSize) + column;
if (bitDepth == Av1BitDepth.EightBit) if (bitDepth == Av1BitDepth.EightBit)
{ {
byte expected = (byte)((firstBytePrediction[predictionIndex] + // Libaom truncates the equal average before removing the Q4 compound bias, then performs the
secondBytePrediction[predictionIndex] + 1) >> 1); // sole final rounding step. Averaging two already reconstructed pixels can differ by one.
int intermediate = ((firstHighBitDepthPrediction[predictionIndex] +
secondHighBitDepthPrediction[predictionIndex]) >> 1) - compoundRoundOffset;
byte expected = (byte)Math.Clamp(
(intermediate + (1 << (compoundRoundBits - 1))) >> compoundRoundBits,
0,
byte.MaxValue);
Span<byte> samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(blockOrigin + row); Span<byte> samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(blockOrigin + row);
Assert.Equal(expected, samples[blockOrigin + column]); Assert.Equal(expected, samples[blockOrigin + column]);

57
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterPredictorTests.cs

@ -101,6 +101,13 @@ public class Av1InterPredictorTests
public void HighBitDepthPredictionMatchesLibaomOracleAcrossIntrinsicWidths() public void HighBitDepthPredictionMatchesLibaomOracleAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthPredictions, PredictorConfigurations); => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthPredictions, PredictorConfigurations);
/// <summary>
/// Verifies that SIMD compound intermediates retain scalar-equivalent values and untouched destination padding.
/// </summary>
[Fact]
public void CompoundPredictionMatchesScalarAcrossIntrinsicWidths()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateCompoundPredictions, PredictorConfigurations);
/// <summary> /// <summary>
/// Applies every byte prediction scenario to the SIMD-first and explicitly scalar entry points. /// Applies every byte prediction scenario to the SIMD-first and explicitly scalar entry points.
/// </summary> /// </summary>
@ -165,12 +172,62 @@ public class Av1InterPredictorTests
} }
} }
/// <summary>
/// Applies every byte prediction scenario to the SIMD-first and scalar compound-intermediate entry points.
/// </summary>
private static void ValidateCompoundPredictions()
{
foreach (PredictionCase testCase in CreatePredictionCases())
{
byte[] source = CreateByteSource(testCase, out int sourceStride, out int sourceOrigin);
int destinationStride = testCase.Width + DestinationRowPadding;
ushort[] expected = CreateHighBitDepthDestination(testCase, destinationStride);
ushort[] actual = (ushort[])expected.Clone();
short[] simdScratch = CreateScratch(testCase);
short[] scalarScratch = CreateScratch(testCase);
Av1InterPredictor.PredictCompoundScalar(
source,
sourceStride,
sourceOrigin,
expected.AsSpan(DestinationPrefix),
destinationStride,
testCase.Width,
testCase.Height,
testCase.HorizontalFilter,
testCase.VerticalFilter,
testCase.HorizontalPhase,
testCase.VerticalPhase,
scalarScratch);
Av1InterPredictor.PredictCompound(
source,
sourceStride,
sourceOrigin,
actual.AsSpan(DestinationPrefix),
destinationStride,
testCase.Width,
testCase.Height,
testCase.HorizontalFilter,
testCase.VerticalFilter,
testCase.HorizontalPhase,
testCase.VerticalPhase,
simdScratch);
AssertEqual(expected, actual, testCase, "SIMD-first compound intermediate");
}
}
/// <summary> /// <summary>
/// Creates the named operation matrix covering copy, each one-dimensional direction, separable filtering, reduced kernels, and vector tails. /// Creates the named operation matrix covering copy, each one-dimensional direction, separable filtering, reduced kernels, and vector tails.
/// </summary> /// </summary>
/// <returns>The prediction scenarios.</returns> /// <returns>The prediction scenarios.</returns>
private static PredictionCase[] CreatePredictionCases() => private static PredictionCase[] CreatePredictionCases() =>
[ [
new("copy-sub8x8-chroma", 2, 4, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Sharp, 0, 0),
new("regular-horizontal-sub8x8-chroma", 2, 4, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 3, 0),
new("regular-vertical-sub8x8-chroma", 4, 2, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 0, 3),
new("smooth-sharp-sub8x8-chroma", 2, 2, Av1InterpolationFilter.Smooth, Av1InterpolationFilter.Sharp, 7, 13),
new("copy-wide-tail", 68, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Sharp, 0, 0), new("copy-wide-tail", 68, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Sharp, 0, 0),
new("regular-horizontal-wide-tail", 68, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 1, 0), new("regular-horizontal-wide-tail", 68, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 1, 0),
new("smooth-horizontal-256-tail", 36, 8, Av1InterpolationFilter.Smooth, Av1InterpolationFilter.Regular, 7, 0), new("smooth-horizontal-256-tail", 36, 8, Av1InterpolationFilter.Smooth, Av1InterpolationFilter.Regular, 7, 0),

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

@ -155,6 +155,37 @@ public class Av1ReconstructionConformanceTests
/// </summary> /// </summary>
private const int AverageCompoundFixtureFrameCount = 19; private const int AverageCompoundFixtureFrameCount = 19;
/// <summary>
/// The displayed width of the official libaom motion-vector sequence.
/// </summary>
private const int OfficialMotionVectorFixtureWidth = 352;
/// <summary>
/// The displayed height of the official libaom motion-vector sequence.
/// </summary>
private const int OfficialMotionVectorFixtureHeight = 288;
/// <summary>
/// The number of shown frames in the official libaom motion-vector sequence.
/// </summary>
private const int OfficialMotionVectorFixtureFrameCount = 4;
/// <summary>
/// The bit mask containing every single-reference and compound inter prediction mode.
/// </summary>
private const int RequiredInterModeCoverage =
(1 << ((int)Av1PredictionMode.InterModeEnd - (int)Av1PredictionMode.InterModeStart)) - 1;
/// <summary>
/// The bit mask containing every simple, OBMC, and locally warped motion mode.
/// </summary>
private const int RequiredMotionModeCoverage = (1 << 3) - 1;
/// <summary>
/// The bit mask containing every regular, smooth, and sharp vertical/horizontal filter pair.
/// </summary>
private const int RequiredSwitchableFilterPairCoverage = (1 << 9) - 1;
/// <summary> /// <summary>
/// The coverage bit representing distance-weighted compound prediction. /// The coverage bit representing distance-weighted compound prediction.
/// </summary> /// </summary>
@ -826,6 +857,144 @@ public class Av1ReconstructionConformanceTests
fixtureSize: 256, fixtureSize: 256,
visibleFrameCount: 2); visibleFrameCount: 2);
/// <summary>
/// Verifies every ordinary inter mode, motion mode, and switchable dual-filter pair against the official
/// pinned-libaom motion-vector conformance sequence and its exact native output.
/// </summary>
[Fact]
public void DecodeOfficialMotionVectorSequenceMatchesPinnedLibaomReference()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(
ValidateOfficialMotionVectorFixtureWithDefaultConfiguration,
ReconstructionConfigurations);
/// <summary>
/// Verifies the official motion-vector conformance sequence through constrained tracked allocation.
/// </summary>
[Fact]
[ValidateDisposedMemoryAllocations]
public void DecodeOfficialMotionVectorSequenceUsesContiguousPlanes()
{
TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 2_048 };
allocator.EnableNonThreadSafeLogging();
Configuration configuration = Configuration.Default.Clone();
configuration.MemoryAllocator = allocator;
ValidateOfficialMotionVectorFixture(configuration);
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.AllocationId == allocation.AllocationId));
}
/// <summary>
/// Runs the official motion-vector fixture with the default decoder configuration.
/// </summary>
private static void ValidateOfficialMotionVectorFixtureWithDefaultConfiguration()
=> ValidateOfficialMotionVectorFixture(Configuration.Default);
/// <summary>
/// Decodes every IVF sample in one retained session, compares each shown frame exactly, and records the
/// syntax selections that make the vector authoritative for ordinary inter-mode and filter coverage.
/// </summary>
private static void ValidateOfficialMotionVectorFixture(Configuration configuration)
{
byte[] ivf = TestFile.Create(TestImages.Heif.Av1OfficialMotionVectorSequence).Bytes;
byte[] nativeReference = TestFile.Create(TestImages.Heif.Av1OfficialMotionVectorSequenceNativeReference).Bytes;
ReadOnlySpan<byte> y4mFileHeader = "YUV4MPEG2 W352 H288 F30:1 Ip C420jpeg\n"u8;
ReadOnlySpan<byte> y4mFrameHeader = "FRAME\n"u8;
Assert.True(ivf.AsSpan(0, 4).SequenceEqual("DKIF"u8));
Assert.Equal(0, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(4, 2)));
Assert.Equal(32, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(6, 2)));
Assert.True(ivf.AsSpan(8, 4).SequenceEqual("AV01"u8));
Assert.Equal(OfficialMotionVectorFixtureWidth, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(12, 2)));
Assert.Equal(OfficialMotionVectorFixtureHeight, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(14, 2)));
Assert.Equal(
OfficialMotionVectorFixtureFrameCount,
checked((int)BinaryPrimitives.ReadUInt32LittleEndian(ivf.AsSpan(24, 4))));
Assert.True(nativeReference.AsSpan().StartsWith(y4mFileHeader));
int ivfOffset = 32;
int nativeOffset = y4mFileHeader.Length;
int nativeFrameLength =
(OfficialMotionVectorFixtureWidth * OfficialMotionVectorFixtureHeight) +
(2 * (OfficialMotionVectorFixtureWidth >> 1) * (OfficialMotionVectorFixtureHeight >> 1));
int interModeCoverage = 0;
int motionModeCoverage = 0;
int switchableFilterPairCoverage = 0;
using Av1Decoder decoder = new(configuration);
for (int frameIndex = 0; frameIndex < OfficialMotionVectorFixtureFrameCount; frameIndex++)
{
int payloadLength = checked((int)BinaryPrimitives.ReadUInt32LittleEndian(ivf.AsSpan(ivfOffset, 4)));
ivfOffset += 12;
ImageFrame<Rgba32> decodedFrame = decoder.DecodeSequenceFrame<Rgba32>(
ivf.AsSpan(ivfOffset, payloadLength),
null,
null);
using ImageFrame<Rgba32> frame = decodedFrame;
ivfOffset += payloadLength;
Assert.Equal(OfficialMotionVectorFixtureWidth, frame.Width);
Assert.Equal(OfficialMotionVectorFixtureHeight, frame.Height);
Assert.True(nativeReference.AsSpan(nativeOffset).StartsWith(y4mFrameHeader));
nativeOffset += y4mFrameHeader.Length;
Av1FrameBuffer<byte> frameBuffer = Assert.IsType<Av1FrameBuffer<byte>>(decoder.FrameBuffer);
Assert.Equal(OfficialMotionVectorFixtureWidth, frameBuffer.Width);
Assert.Equal(OfficialMotionVectorFixtureHeight, frameBuffer.Height);
Assert.Equal(Av1BitDepth.EightBit, frameBuffer.BitDepth);
Assert.Equal(Av1ColorFormat.Yuv420, frameBuffer.ColorFormat);
AssertNativePlanesEqual(
decoder,
frameBuffer,
nativeReference.AsSpan(nativeOffset, nativeFrameLength));
nativeOffset += nativeFrameLength;
ObuSequenceHeader sequenceHeader = Assert.IsType<ObuSequenceHeader>(decoder.SequenceHeader);
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
int superblockColumnCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, sequenceHeader.SuperblockSizeLog2)
>> sequenceHeader.SuperblockSizeLog2;
int superblockRowCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameHeight, sequenceHeader.SuperblockSizeLog2)
>> sequenceHeader.SuperblockSizeLog2;
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.YMode is < Av1PredictionMode.InterModeStart or >= Av1PredictionMode.InterModeEnd)
{
continue;
}
interModeCoverage |= 1 << ((int)modeInfo.YMode - (int)Av1PredictionMode.InterModeStart);
motionModeCoverage |= 1 << (int)modeInfo.MotionMode;
int verticalFilter = (int)modeInfo.InterpolationFilters[0];
int horizontalFilter = (int)modeInfo.InterpolationFilters[1];
switchableFilterPairCoverage |= 1 << ((verticalFilter * 3) + horizontalFilter);
}
}
}
}
Assert.Equal(ivf.Length, ivfOffset);
Assert.Equal(nativeReference.Length, nativeOffset);
Assert.Equal(RequiredInterModeCoverage, interModeCoverage);
Assert.Equal(RequiredMotionModeCoverage, motionModeCoverage);
Assert.Equal(RequiredSwitchableFilterPairCoverage, switchableFilterPairCoverage);
}
/// <summary> /// <summary>
/// Verifies one complete inter-prediction sequence with a separately tracked constrained allocator. /// Verifies one complete inter-prediction sequence with a separately tracked constrained allocator.
/// </summary> /// </summary>
@ -3032,9 +3201,12 @@ public class Av1ReconstructionConformanceTests
{ {
if (expected != actual) if (expected != actual)
{ {
ObuSequenceHeader sequenceHeader = Assert.IsType<ObuSequenceHeader>(decoder.SequenceHeader);
int subsamplingX = plane == Av1Plane.Y || !sequenceHeader.ColorConfig.SubSamplingX ? 0 : 1;
int subsamplingY = plane == Av1Plane.Y || !sequenceHeader.ColorConfig.SubSamplingY ? 0 : 1;
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo); Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
int modeInfoColumn = x >> Av1Constants.ModeInfoSizeLog2; int modeInfoColumn = (x << subsamplingX) >> Av1Constants.ModeInfoSizeLog2;
int modeInfoRow = y >> Av1Constants.ModeInfoSizeLog2; int modeInfoRow = (y << subsamplingY) >> Av1Constants.ModeInfoSizeLog2;
Av1BlockModeInfo modeInfo = frameInfo.GetModeInfoAt(new Point(modeInfoColumn, modeInfoRow)); Av1BlockModeInfo modeInfo = frameInfo.GetModeInfoAt(new Point(modeInfoColumn, modeInfoRow));
int blockColumn = modeInfoColumn; int blockColumn = modeInfoColumn;
while (blockColumn > 0 && ReferenceEquals(frameInfo.GetModeInfoAt(new Point(blockColumn - 1, modeInfoRow)), modeInfo)) while (blockColumn > 0 && ReferenceEquals(frameInfo.GetModeInfoAt(new Point(blockColumn - 1, modeInfoRow)), modeInfo))
@ -3116,6 +3288,10 @@ public class Av1ReconstructionConformanceTests
$"Plane {plane} differs at ({x}, {y}): expected {expected}, actual {actual}. " $"Plane {plane} differs at ({x}, {y}): expected {expected}, actual {actual}. "
+ $"Total unequal samples={mismatchCount}:{mismatchDescription}. " + $"Total unequal samples={mismatchCount}:{mismatchDescription}. "
+ $"Block={modeInfo.BlockSize}, mode={modeInfo.YMode}, partition={modeInfo.PartitionType}, skip={modeInfo.Skip}, " + $"Block={modeInfo.BlockSize}, mode={modeInfo.YMode}, partition={modeInfo.PartitionType}, skip={modeInfo.Skip}, "
+ $"refs={modeInfo.ReferenceFrames[0]}/{modeInfo.ReferenceFrames[1]}, "
+ $"mvs={modeInfo.MotionVectors[0].Row},{modeInfo.MotionVectors[0].Column}/"
+ $"{modeInfo.MotionVectors[1].Row},{modeInfo.MotionVectors[1].Column}, compound={modeInfo.CompoundType}, "
+ $"filters={modeInfo.InterpolationFilters[0]}/{modeInfo.InterpolationFilters[1]}, motion={modeInfo.MotionMode}, "
+ $"filter-intra={modeInfo.UseFilterIntra}/{modeInfo.FilterIntraMode}, angle-delta={modeInfo.GetAngleDelta(plane)}, " + $"filter-intra={modeInfo.UseFilterIntra}/{modeInfo.FilterIntraMode}, angle-delta={modeInfo.GetAngleDelta(plane)}, "
+ $"palette-size={modeInfo.GetPaletteSize(plane)}, transforms={modeInfo.GetTransformUnitCount(plane)}, " + $"palette-size={modeInfo.GetPaletteSize(plane)}, transforms={modeInfo.GetTransformUnitCount(plane)}, "
+ $"transform={containingTransform.Size}/{containingTransform.Type}/coded={containingTransform.CodeBlockFlag} " + $"transform={containingTransform.Size}/{containingTransform.Type}/coded={containingTransform.CodeBlockFlag} "

2
tests/ImageSharp.Tests/TestImages.cs

@ -1386,6 +1386,8 @@ public static class TestImages
public const string Av1GlobalWarpSequenceAvif = "Heif/Av1/Conformance/libavif-rotating-grid-global-warp.avif"; public const string Av1GlobalWarpSequenceAvif = "Heif/Av1/Conformance/libavif-rotating-grid-global-warp.avif";
public const string Av1GlobalWarpSequenceNativeReference = "Heif/Av1/Conformance/libavif-rotating-grid-global-warp-libaom.y4m"; public const string Av1GlobalWarpSequenceNativeReference = "Heif/Av1/Conformance/libavif-rotating-grid-global-warp-libaom.y4m";
public const string Av1GlobalWarpSequencePresentationReference = "Heif/Av1/Conformance/libavif-rotating-grid-global-warp-libavif.png"; public const string Av1GlobalWarpSequencePresentationReference = "Heif/Av1/Conformance/libavif-rotating-grid-global-warp-libavif.png";
public const string Av1OfficialMotionVectorSequence = "Heif/Av1/Conformance/libaom-av1-1-b8-05-mv.ivf";
public const string Av1OfficialMotionVectorSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b8-05-mv-libaom.y4m";
public const string Av1Cdef8BitPayload = "Heif/Av1/Conformance/libaom-cdef-kodim23-8b.bit"; 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 Av1Cdef8BitReference = "Heif/Av1/Conformance/libaom-cdef-kodim23-8b-libaom.yuv";
public const string Av1Cdef8BitAvif = "Heif/Av1/Conformance/libavif-cdef-kodim23-8b.avif"; public const string Av1Cdef8BitAvif = "Heif/Av1/Conformance/libavif-cdef-kodim23-8b.avif";

3
tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-05-mv-libaom.y4m

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

3
tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-05-mv.ivf

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