From 1c58d855f70b024170ced9eb0a7005f0f9c955ad Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 29 Aug 2026 09:51:51 +1000 Subject: [PATCH] Complete official AV1 motion-vector conformance --- .gitattributes | 2 + HEIF_IMPLEMENTATION_PLAN.md | 26 +- .../Formats/Heif/Av1/Av1Constants.cs | 2 +- .../Av1CompoundInterPredictor.Intermediate.cs | 529 +++++++++++++ .../Inter/Av1InterPredictor.Compound.cs | 732 ++++++++++++++++++ .../Inter/Av1InterPredictor.Dispatch.cs | 5 +- .../Inter/Av1InterPredictor.OneDimension.cs | 32 +- .../Av1InterPredictor.TwoDimensions.UInt16.cs | 9 +- .../Inter/Av1InterPredictor.Warped.cs | 170 ++++ .../Formats/Heif/Av1/Tiling/Av1TileReader.cs | 4 +- .../Heif/Av1/Transform/Av1BlockDecoder.cs | 468 +++++++++-- .../Heif/Av1/Av1CompoundBlockDecoderTests.cs | 23 +- .../Heif/Av1/Av1InterPredictorTests.cs | 57 ++ .../Av1/Av1ReconstructionConformanceTests.cs | 180 ++++- tests/ImageSharp.Tests/TestImages.cs | 2 + .../libaom-av1-1-b8-05-mv-libaom.y4m | 3 + .../Av1/Conformance/libaom-av1-1-b8-05-mv.ivf | 3 + 17 files changed, 2170 insertions(+), 77 deletions(-) create mode 100644 src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Intermediate.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Compound.cs create mode 100644 tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-05-mv-libaom.y4m create mode 100644 tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-05-mv.ivf diff --git a/.gitattributes b/.gitattributes index 4b946243c..e594c0e9b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -140,6 +140,8 @@ *.avif filter=lfs diff=lfs merge=lfs -text *.hvc1 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 *.bit filter=lfs diff=lfs merge=lfs -text ############################################################################### diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 273cb2980..07c926331 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -29,7 +29,9 @@ Checkboxes may be marked complete only when the implementation and the verificat ## Delivery dashboard -Last reconciled with the source tree on 2026-08-29 against 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: @@ -39,15 +41,15 @@ Status meanings: - **Not started:** supporting primitives may exist, but the production format path is absent. - **Current:** the only work item that should be advanced before taking the next queued item. -Current development stage: **Stage 3 — complete AV1 still-image decoding.** The decoder retains reference/header/CDF/motion-field state, derives frame-level skip-mode references, consumes temporal segment prediction, decodes intra-coded blocks inside inter frames, 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 | | --- | --- | --- | --- | --- | | 1 | Baseline, provenance, documentation, and public contract | In progress | Pinned codec references, a bounded image-only scope, encoder options, typed bit depth, decoder-option propagation, and extensive HEIF documentation exist. | Complete the all-file documentation audit, record a fresh Release baseline, finish distinct public HEIC/AVIF save boundaries, and close API review. | | 2 | Bounded HEIF item and image-sequence container | In progress | Still-item parsing, grids, auxiliary alpha, metadata properties, bounded image-sequence tracks, Identify, and all-sync AV1 sequence presentation are connected. | Complete adversarial boundary coverage, remaining item/property behavior, reference-dependent sequence reconstruction, and the bounded sequence writer. | -| 3 | Still-image AV1 and HEVC decoding | **Current** | HEVC reconstruction reaches exact HM/libheif fixtures across the recorded 8/10/12-bit and chroma cases. AV1 work includes bounded OBU framing, reconstruction, filters, grain, color, transforms, intra-block copy, an exact independent 12-profile bit-depth/chroma matrix through every dispatch tier, retained layered reference/header/CDF/motion-field state, temporal segment prediction, inter-frame intra blocks, SIMD-first translational prediction, exact single-reference inter reconstruction, 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. | | 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. | @@ -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 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 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. - - [ ] **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 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. + - [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. - [ ] 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. @@ -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 `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. -- 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. - 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. @@ -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 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 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. - - [ ] **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 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. + - [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. - [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. @@ -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 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 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. -- [ ] **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. +- [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. +- [ ] **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 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. diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs b/src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs index 82f11fb41..72822fbe0 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs @@ -194,7 +194,7 @@ internal static class Av1Constants /// /// The number of low-order bits reserved for a cumulative coefficient-level context. /// - public const int CoefficientContextBitCount = 6; + public const int CoefficientContextBitCount = 3; /// /// The mask selecting the cumulative coefficient-level magnitude bits. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Intermediate.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Intermediate.cs new file mode 100644 index 000000000..0e126bbef --- /dev/null +++ b/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; + +/// +/// Combines high-precision compound convolution intermediates into reconstructed samples. +/// +internal static partial class Av1CompoundInterPredictor +{ + /// + /// Combines two compound intermediates by equal averaging. + /// + public static void AverageIntermediate( + Span destination, + int destinationStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan 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 destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan firstRow = first.Slice(row * firstStride, width); + ReadOnlySpan 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.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 first0 = Vector128.LoadUnsafe(ref firstReference, (nuint)column); + Vector128 first1 = Vector128.LoadUnsafe( + ref firstReference, + (nuint)(column + Vector128.Count)); + + Vector128 second0 = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + Vector128 second1 = Vector128.LoadUnsafe( + ref secondReference, + (nuint)(column + Vector128.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); + } + } + } + + /// + /// Combines two compound intermediates using the decoded temporal-distance weights. + /// + public static void DistanceWeightedIntermediate( + Span destination, + int destinationStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan 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 destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan firstRow = first.Slice(row * firstStride, width); + ReadOnlySpan 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.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 first0 = Vector128.LoadUnsafe(ref firstReference, (nuint)column); + Vector128 first1 = Vector128.LoadUnsafe( + ref firstReference, + (nuint)(column + Vector128.Count)); + + Vector128 second0 = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + Vector128 second1 = Vector128.LoadUnsafe( + ref secondReference, + (nuint)(column + Vector128.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); + } + } + } + + /// + /// Fills a luma-resolution difference-weighted mask from compound intermediates. + /// + public static void FillDifferenceWeightedIntermediateMask( + Span mask, + int maskStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan 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 maskRow = mask.Slice(row * maskStride, width); + ReadOnlySpan firstRow = first.Slice(row * firstStride, width); + ReadOnlySpan 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.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 first0 = Vector128.LoadUnsafe(ref firstReference, (nuint)column); + Vector128 first1 = Vector128.LoadUnsafe( + ref firstReference, + (nuint)(column + Vector128.Count)); + + Vector128 second0 = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + Vector128 second1 = Vector128.LoadUnsafe( + ref secondReference, + (nuint)(column + Vector128.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); + } + } + } + + /// + /// Blends two compound intermediates through a luma-resolution mask. + /// + public static void BlendIntermediate( + Span destination, + int destinationStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + ReadOnlySpan 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 destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan firstRow = first.Slice(row * firstStride, width); + ReadOnlySpan 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.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 first0 = Vector128.LoadUnsafe(ref firstReference, (nuint)column); + Vector128 first1 = Vector128.LoadUnsafe( + ref firstReference, + (nuint)(column + Vector128.Count)); + + Vector128 second0 = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + Vector128 second1 = Vector128.LoadUnsafe( + ref secondReference, + (nuint)(column + Vector128.Count)); + + Vector128 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); + } + } + } + + /// + /// Equal-averages sixteen compound lanes and converts them to final 8-bit samples. + /// + private static Vector128 AverageIntermediate( + Vector128 first0, + Vector128 first1, + Vector128 second0, + Vector128 second1, + int roundBits, + int roundOffset) + => Vector128.Narrow( + FinalizeIntermediate( + (first0 & second0) + ((first0 ^ second0) >> 1), + roundBits, + roundOffset), + FinalizeIntermediate( + (first1 & second1) + ((first1 ^ second1) >> 1), + roundBits, + roundOffset)); + + /// + /// Distance-weights sixteen compound lanes and converts them to final 8-bit samples. + /// + private static Vector128 DistanceWeightedIntermediate( + Vector128 first0, + Vector128 first1, + Vector128 second0, + Vector128 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)); + + /// + /// Distance-weights eight compound lanes without overflowing the unsigned intermediate range. + /// + private static Vector128 DistanceWeightedIntermediate( + Vector128 first, + Vector128 second, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset) + { + Vector128 firstLower = Vector128.WidenLower(first).AsInt32(); + Vector128 firstUpper = Vector128.WidenUpper(first).AsInt32(); + Vector128 secondLower = Vector128.WidenLower(second).AsInt32(); + Vector128 secondUpper = Vector128.WidenUpper(second).AsInt32(); + Vector128 lower = + ((firstLower * firstWeight) + (secondLower * secondWeight)) >> DistanceWeightBits; + + Vector128 upper = + ((firstUpper * firstWeight) + (secondUpper * secondWeight)) >> DistanceWeightBits; + + return Vector128.Narrow( + FinalizeIntermediate(lower, roundBits, roundOffset), + FinalizeIntermediate(upper, roundBits, roundOffset)).AsUInt16(); + } + + /// + /// Creates sixteen difference-weighted mask values from compound intermediates. + /// + private static Vector128 DifferenceWeightedIntermediate( + Vector128 first0, + Vector128 first1, + Vector128 second0, + Vector128 second1, + int differenceRound, + bool invert) + => Vector128.Narrow( + DifferenceWeightedIntermediate(first0, second0, differenceRound, invert), + DifferenceWeightedIntermediate(first1, second1, differenceRound, invert)); + + /// + /// Creates eight difference-weighted mask values without losing the required pre-alpha rounding. + /// + private static Vector128 DifferenceWeightedIntermediate( + Vector128 first, + Vector128 second, + int differenceRound, + bool invert) + { + Vector128 difference = Vector128.Max(first, second) - Vector128.Min(first, second); + Vector128 lower = DifferenceWeightedIntermediate( + Vector128.WidenLower(difference).AsInt32(), + differenceRound, + invert); + + Vector128 upper = DifferenceWeightedIntermediate( + Vector128.WidenUpper(difference).AsInt32(), + differenceRound, + invert); + + return Vector128.Narrow(lower, upper).AsUInt16(); + } + + /// + /// Converts four intermediate differences to the decoded type-38 mask range. + /// + private static Vector128 DifferenceWeightedIntermediate( + Vector128 difference, + int differenceRound, + bool invert) + { + if (differenceRound != 0) + { + difference = (difference + Vector128.Create(1 << (differenceRound - 1))) >> differenceRound; + } + + Vector128 maximum = Vector128.Create(MaximumMaskAlpha); + Vector128 alpha = Vector128.Min(maximum, (difference >> 4) + Vector128.Create(38)); + return invert ? maximum - alpha : alpha; + } + + /// + /// Mask-blends sixteen compound lanes and converts them to final 8-bit samples. + /// + private static Vector128 BlendIntermediate( + Vector128 first0, + Vector128 first1, + Vector128 second0, + Vector128 second1, + Vector128 alpha, + int roundBits, + int roundOffset) + => Vector128.Narrow( + BlendIntermediate( + first0, + second0, + Vector128.WidenLower(alpha), + roundBits, + roundOffset), + BlendIntermediate( + first1, + second1, + Vector128.WidenUpper(alpha), + roundBits, + roundOffset)); + + /// + /// Mask-blends eight compound lanes after widening every product to signed 32-bit precision. + /// + private static Vector128 BlendIntermediate( + Vector128 first, + Vector128 second, + Vector128 alpha, + int roundBits, + int roundOffset) + { + Vector128 firstLower = Vector128.WidenLower(first).AsInt32(); + Vector128 firstUpper = Vector128.WidenUpper(first).AsInt32(); + Vector128 secondLower = Vector128.WidenLower(second).AsInt32(); + Vector128 secondUpper = Vector128.WidenUpper(second).AsInt32(); + Vector128 alphaLower = Vector128.WidenLower(alpha).AsInt32(); + Vector128 alphaUpper = Vector128.WidenUpper(alpha).AsInt32(); + Vector128 maximum = Vector128.Create(MaximumMaskAlpha); + Vector128 lower = + ((alphaLower * firstLower) + ((maximum - alphaLower) * secondLower)) >> MaskWeightBits; + + Vector128 upper = + ((alphaUpper * firstUpper) + ((maximum - alphaUpper) * secondUpper)) >> MaskWeightBits; + + return Vector128.Narrow( + FinalizeIntermediate(lower, roundBits, roundOffset), + FinalizeIntermediate(upper, roundBits, roundOffset)).AsUInt16(); + } + + /// + /// Removes the compound bias and final fractional precision from eight unsigned lanes. + /// + private static Vector128 FinalizeIntermediate( + Vector128 value, + int roundBits, + int roundOffset) + { + Vector128 result = (value - Vector128.Create((ushort)roundOffset)).AsInt16(); + if (roundBits != 0) + { + result = (result + Vector128.Create((short)(1 << (roundBits - 1)))) >> roundBits; + } + + result = Vector128.Max(Vector128.Zero, Vector128.Min(Vector128.Create((short)byte.MaxValue), result)); + return result.AsUInt16(); + } + + /// + /// Removes the compound bias and final fractional precision from four widened lanes. + /// + private static Vector128 FinalizeIntermediate( + Vector128 value, + int roundBits, + int roundOffset) + { + Vector128 result = value - Vector128.Create(roundOffset); + if (roundBits != 0) + { + result = (result + Vector128.Create(1 << (roundBits - 1))) >> roundBits; + } + + return Vector128.Max(Vector128.Zero, Vector128.Min(Vector128.Create((int)byte.MaxValue), result)); + } + + /// + /// Gets the mask alpha for one plane sample, averaging its two or four luma samples when required. + /// + private static int GetSubsampledMaskAlpha( + ReadOnlySpan 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); + } + + /// + /// Derives the bias and remaining fractional precision of a compound intermediate. + /// + 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)); + } + + /// + /// Applies AV1's positive power-of-two rounding rule. + /// + private static int RoundPowerOfTwo(int value, int bits) + => bits == 0 ? value : (value + (1 << (bits - 1))) >> bits; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Compound.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Compound.cs new file mode 100644 index 000000000..7327a2c34 --- /dev/null +++ b/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; + +/// +/// Produces the biased high-precision intermediates required by compound inter prediction. +/// +internal static partial class Av1InterPredictor +{ + /// + /// The second-round shift retained by every compound convolution path. + /// + internal const int CompoundRound1Bits = 7; + + /// + /// Reconstructs one 8-bit translational reference into AV1's unsigned compound intermediate format. + /// + public static void PredictCompound( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + Span 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); + } + + /// + /// Reconstructs one compound intermediate without explicit hardware intrinsics. + /// + public static void PredictCompoundScalar( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + Span scratch) + { + ReadOnlySpan horizontalCoefficients = GetCompoundCoefficients(horizontalFilter, horizontalPhase, width <= 4); + ReadOnlySpan 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); + } + } + } + + /// + /// Reconstructs one compound intermediate through the 128-bit convolution tier. + /// + private static void PredictCompoundVector128( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + Span scratch) + { + ReadOnlySpan horizontalCoefficients = GetCompoundCoefficients(horizontalFilter, horizontalPhase, width <= 4); + ReadOnlySpan 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); + } + + /// + /// Copies integer-position samples into biased compound intermediates in sixteen-sample groups. + /// + private static void CopyCompoundVector128( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span 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 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.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 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.Count)); + } + + if (column == 0) + { + // Narrow AV1 blocks still use the SIMD load; the width-specific stores preserve the adjacent block. + Vector128 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); + } + } + } + + /// + /// Applies one compound convolution direction in sixteen-sample groups. + /// + private static void FilterCompoundDirectVector128( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan 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 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.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Convolve( + ref sourceRow, + tapStride, + (nuint)column, + ref coefficientBase, + tapCount, + Vector128.Zero, + out Vector128 result0, + out Vector128 result1, + out Vector128 result2, + out Vector128 result3); + + PrepareCompoundResults( + ref result0, + ref result1, + ref result2, + ref result3, + preShift, + round, + offset); + + StoreCompoundVectors(result0, result1, result2, result3, ref destinationRow, column, Vector128.Count); + } + + if (column == 0) + { + Convolve( + ref sourceRow, + tapStride, + 0, + ref coefficientBase, + tapCount, + Vector128.Zero, + out Vector128 result0, + out Vector128 result1, + out Vector128 result2, + out Vector128 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; + } + } + } + + /// + /// Applies separable compound convolution through caller-owned signed scratch. + /// + private static void FilterCompound2DVector128( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan horizontalCoefficients, + int horizontalTapCount, + int horizontalSourceOffset, + ReadOnlySpan verticalCoefficients, + int verticalTapCount, + int verticalSourceOffset, + Span 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 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.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Convolve( + ref sourceRow, + 1, + (nuint)column, + ref horizontalCoefficientBase, + horizontalTapCount, + horizontalBias, + out Vector128 result0, + out Vector128 result1, + out Vector128 result2, + out Vector128 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.Count)); + } + + if (column == 0) + { + Convolve( + ref sourceRow, + 1, + 0, + ref horizontalCoefficientBase, + horizontalTapCount, + horizontalBias, + out Vector128 result0, + out Vector128 result1, + out Vector128 result2, + out Vector128 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.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 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.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Convolve( + ref scratchRow, + scratchStride, + (nuint)column, + ref verticalCoefficientBase, + verticalTapCount, + verticalBias, + out Vector128 result0, + out Vector128 result1); + + Convolve( + ref scratchRow, + scratchStride, + (nuint)(column + Vector128.Count), + ref verticalCoefficientBase, + verticalTapCount, + verticalBias, + out Vector128 result2, + out Vector128 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.Count); + } + + if (column == 0) + { + Convolve( + ref scratchRow, + scratchStride, + 0, + ref verticalCoefficientBase, + verticalTapCount, + verticalBias, + out Vector128 result0, + out Vector128 result1); + + Convolve( + ref Unsafe.Add(ref scratchRow, Vector128.Count), + scratchStride, + 0, + ref verticalCoefficientBase, + verticalTapCount, + verticalBias, + out Vector128 result2, + out Vector128 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); + } + } + } + + /// + /// Applies the compound direct-filter shifts and bias to sixteen convolution results. + /// + private static void PrepareCompoundResults( + ref Vector128 result0, + ref Vector128 result1, + ref Vector128 result2, + ref Vector128 result3, + int preShift, + int round, + Vector128 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; + } + + /// + /// Packs and stores up to sixteen unsigned compound results. + /// + private static void StoreCompoundVectors( + Vector128 result0, + Vector128 result1, + Vector128 result2, + Vector128 result3, + ref ushort destination, + int destinationOffset, + int width) + { + Vector128 lower = Av1IntraPredictorBase.Narrow(result0, result1).AsUInt16(); + Vector128 upper = Av1IntraPredictorBase.Narrow(result2, result3).AsUInt16(); + ref ushort destinationStart = ref Unsafe.Add(ref destination, destinationOffset); + StoreCompoundVectors(lower, upper, ref destinationStart, width); + } + + /// + /// Stores up to sixteen packed compound results without crossing the logical block edge. + /// + private static void StoreCompoundVectors( + Vector128 lower, + Vector128 upper, + ref ushort destination, + int width) + { + int lowerWidth = Math.Min(width, Vector128.Count); + StorePartial(lower, ref destination, lowerWidth); + if (width > Vector128.Count) + { + StorePartial( + upper, + ref Unsafe.Add(ref destination, Vector128.Count), + width - Vector128.Count); + } + } + + /// + /// Gets the selected interpolation kernel for compound traversal. + /// + private static ReadOnlySpan 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), + }; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Dispatch.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Dispatch.cs index 29c82b4ae..1f5afd1cb 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Dispatch.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Dispatch.cs @@ -517,8 +517,9 @@ internal static partial class Av1InterPredictor if (width < Vector128.Count) { - // Four high-bit-depth samples occupy exactly the lower 64 bits of the vector. - Vector128.LoadUnsafe(ref sourceRow).GetLower().StoreUnsafe(ref destinationRow); + // Subsampled sub-8x8 chroma can be two samples wide, so retain the vector load while limiting + // the store to the logical row width. + StorePartial(Vector128.LoadUnsafe(ref sourceRow), ref destinationRow, width); continue; } diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.OneDimension.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.OneDimension.cs index 9bad7069c..594aac772 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.OneDimension.cs +++ b/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 result0, out Vector128 result1); Round(ref result0, ref result1, firstRound, secondRound); - // The only legal AV1 width below eight is four samples, exactly the lower Vector64 half. - PackHighBitDepth(result0, result1, maximum).GetLower().StoreUnsafe(ref destinationRow); + // Subsampled sub-8x8 chroma can be two samples wide. Keep the full source load for throughput, but + // store only the logical row so the adjacent luma owner's chroma prediction remains intact. + StorePartial(PackHighBitDepth(result0, result1, maximum), ref destinationRow, width); continue; } @@ -439,7 +440,7 @@ internal static partial class Av1InterPredictor } /// - /// 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. /// private static void StorePartial(Vector128 value, ref byte destination, int width) { @@ -447,10 +448,33 @@ internal static partial class Av1InterPredictor { value.GetLower().StoreUnsafe(ref destination); } - else + else if (width == 4) { Unsafe.As(ref destination) = value.AsUInt32().GetElement(0); } + else + { + Unsafe.As(ref destination) = value.AsUInt16().GetElement(0); + } + } + + /// + /// Stores the two-, four-, or eight-sample prefix of an eight-ushort prediction vector. + /// + private static void StorePartial(Vector128 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(ref destination) = value.AsUInt32().GetElement(0); + } } /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.TwoDimensions.UInt16.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.TwoDimensions.UInt16.cs index 9c3fde100..691a1c5d4 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.TwoDimensions.UInt16.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.TwoDimensions.UInt16.cs @@ -55,8 +55,8 @@ internal static partial class Av1InterPredictor if (width < Vector128.Count) { - // AV1's only legal width below eight is four. The reference plane is padded for the full source load, - // and the minimum scratch stride preserves all eight intermediate lanes needed by the vertical pass. + // Subsampled sub-8x8 chroma can be two samples wide. The reference plane is padded for the full source + // load, and the minimum scratch stride preserves all eight intermediate lanes for the vertical pass. Convolve( ref sourceRow, 1, @@ -132,8 +132,9 @@ internal static partial class Av1InterPredictor result0 = RoundPowerOfTwo(result0, round1) - roundOffset; result1 = RoundPowerOfTwo(result1, round1) - roundOffset; - // Four high-bit-depth samples occupy exactly the lower half of the packed vector. - PackHighBitDepth(result0, result1, maximum).GetLower().StoreUnsafe(ref destinationRow); + // Retain the vector convolution for two- and four-sample rows while leaving adjacent destination + // samples owned by the neighboring luma block untouched. + StorePartial(PackHighBitDepth(result0, result1, maximum), ref destinationRow, width); continue; } diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Warped.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Warped.cs index 3a725c9ca..fcf50b2bb 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Warped.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Warped.cs @@ -126,6 +126,63 @@ internal static partial class Av1InterPredictor scratch); } + /// + /// Reconstructs an 8-bit affine warped reference into AV1's unsigned compound intermediate format. + /// + public static void PredictWarpedCompound( + ReadOnlySpan source, + int sourceStride, + Point sourceOrigin, + int sourceWidth, + int sourceHeight, + Span destination, + int destinationStride, + Point destinationPosition, + int width, + int height, + int subsamplingX, + int subsamplingY, + Av1GlobalMotionParameters parameters, + Span scratch) + { + if (Vector128.IsHardwareAccelerated) + { + PredictWarpedCompound( + source, + sourceStride, + sourceOrigin, + sourceWidth, + sourceHeight, + destination, + destinationStride, + destinationPosition, + width, + height, + subsamplingX, + subsamplingY, + parameters, + scratch); + + return; + } + + PredictWarpedCompound( + source, + sourceStride, + sourceOrigin, + sourceWidth, + sourceHeight, + destination, + destinationStride, + destinationPosition, + width, + height, + subsamplingX, + subsamplingY, + parameters, + scratch); + } + /// /// Reconstructs a high-bit-depth affine warped prediction using the widest supported convolution operator. /// @@ -220,6 +277,40 @@ internal static partial class Av1InterPredictor parameters, scratch); + /// + /// Reconstructs an 8-bit affine warped reference into compound intermediates without explicit hardware intrinsics. + /// + public static void PredictWarpedCompoundScalar( + ReadOnlySpan source, + int sourceStride, + Point sourceOrigin, + int sourceWidth, + int sourceHeight, + Span destination, + int destinationStride, + Point destinationPosition, + int width, + int height, + int subsamplingX, + int subsamplingY, + Av1GlobalMotionParameters parameters, + Span scratch) + => PredictWarpedCompound( + source, + sourceStride, + sourceOrigin, + sourceWidth, + sourceHeight, + destination, + destinationStride, + destinationPosition, + width, + height, + subsamplingX, + subsamplingY, + parameters, + scratch); + /// /// Reconstructs a high-bit-depth affine warped prediction without explicit hardware intrinsics. /// @@ -335,6 +426,85 @@ internal static partial class Av1InterPredictor } } + /// + /// Reconstructs one 8-bit warped reference without discarding the compound convolution precision. + /// + private static void PredictWarpedCompound( + ReadOnlySpan source, + int sourceStride, + Point sourceOrigin, + int sourceWidth, + int sourceHeight, + Span destination, + int destinationStride, + Point destinationPosition, + int width, + int height, + int subsamplingX, + int subsamplingY, + Av1GlobalMotionParameters parameters, + Span scratch) + where TConvolution : struct, IWarpedConvolution + { + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + Span intermediate = MemoryMarshal.Cast(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; + } + } + } + } + } + /// /// Reconstructs one high-bit-depth warped block through a closed convolution operator. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs index c8dcb1dca..e2a66db8d 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs @@ -826,7 +826,9 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable /// Corresponds to reset_skip_context in SVT-AV1. 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++) { int subX = (i > 0 && this.SequenceHeader.ColorConfig.SubSamplingX) ? 1 : 0; diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs index 40df19760..e9abf004b 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs @@ -133,9 +133,11 @@ internal sealed class Av1BlockDecoder : IDisposable Av1InterPredictor.GetScratchLength(maximumBlockLength, maximumBlockLength), Av1InterPredictor.GetMaximumScaledScratchLength(maximumBlockLength, maximumBlockLength))); - // Compound prediction retains the complete second predictor until both references have been sampled. - // Reserve it once beside the convolution workspace so block traversal never rents temporary memory. - int predictionScratchLength = maximumBlockArea + predictorWorkingLength; + int compoundMaskLength = (maximumBlockArea + 1) >> 1; + + // 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(predictionScratchLength); this.inverseQuantizationOwner = inverseQuantizationOwner; @@ -144,7 +146,7 @@ internal sealed class Av1BlockDecoder : IDisposable this.predictionDecoder = new( sequenceHeader, frameHeader, - predictionScratchOwner.Memory[maximumBlockArea..]); + predictionScratchOwner.Memory[((2 * maximumBlockArea) + compoundMaskLength)..]); this.isLoopFilterEnabled = frameHeader.LoopFilterParameters.FilterLevel[0] != 0 || frameHeader.LoopFilterParameters.FilterLevel[1] != 0; @@ -327,13 +329,49 @@ internal sealed class Av1BlockDecoder : IDisposable int predictionHeight = Math.Max(4, blockSize.GetHeight() >> subY); int maximumBlockLength = 1 << this.sequenceHeader.SuperblockSizeLog2; int maximumBlockArea = maximumBlockLength * maximumBlockLength; + int compoundMaskStorageLength = (maximumBlockArea + 1) >> 1; Span predictionStorage = this.predictionScratchOwner.Memory.Span; Span secondPredictionStorage = predictionStorage[..maximumBlockArea]; - Span predictionScratch = predictionStorage[maximumBlockArea..]; + Span firstCompoundPrediction = MemoryMarshal.Cast( + predictionStorage.Slice(maximumBlockArea, maximumBlockArea)); + + Span compoundMask = MemoryMarshal.AsBytes( + predictionStorage.Slice(2 * maximumBlockArea, compoundMaskStorageLength))[..(blockSize.GetWidth() * blockSize.GetHeight())]; + + Span predictionScratch = + predictionStorage[((2 * maximumBlockArea) + compoundMaskStorageLength)..]; + Span secondPrediction = MemoryMarshal.AsBytes(secondPredictionStorage)[..(predictionWidth * predictionHeight)]; Span highBitDepthSecondPrediction = MemoryMarshal.Cast(secondPredictionStorage)[..(predictionWidth * predictionHeight)]; - Span compoundMask = MemoryMarshal.AsBytes(predictionScratch)[..(predictionWidth * predictionHeight)]; - int referenceCount = isCompound ? 2 : 1; + bool usesSub8x8ChromaPrediction = + 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++) { @@ -342,7 +380,9 @@ internal sealed class Av1BlockDecoder : IDisposable : secondaryReferenceFrameBuffer!; 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 || activeReferenceFrameBuffer.Height != this.frameHeader.FrameSize.FrameHeight; @@ -430,25 +470,50 @@ internal sealed class Av1BlockDecoder : IDisposable out int sourceStride, out Point sourceOrigin); - Span destination = referenceIndex == 0 - ? blockReconstructionBuffer[reconstructionStride..] - : secondPrediction; - - Av1InterPredictor.PredictWarped( - source, - sourceStride, - sourceOrigin, - referencePlaneWidth, - referencePlaneHeight, - destination, - destinationStride, - pixelPosition, - predictionWidth, - predictionHeight, - subX, - subY, - warpedMotionParameters, - predictionScratch); + if (useCompoundIntermediates) + { + Span destination = referenceIndex == 0 + ? firstCompoundPrediction + : highBitDepthSecondPrediction; + + Av1InterPredictor.PredictWarpedCompound( + source, + sourceStride, + sourceOrigin, + referencePlaneWidth, + referencePlaneHeight, + destination, + destinationStride, + pixelPosition, + predictionWidth, + predictionHeight, + subX, + subY, + warpedMotionParameters, + predictionScratch); + } + else + { + Span destination = referenceIndex == 0 + ? blockReconstructionBuffer[reconstructionStride..] + : secondPrediction; + + Av1InterPredictor.PredictWarped( + source, + sourceStride, + sourceOrigin, + referencePlaneWidth, + referencePlaneHeight, + destination, + destinationStride, + pixelPosition, + predictionWidth, + predictionHeight, + subX, + subY, + warpedMotionParameters, + predictionScratch); + } } continue; @@ -564,29 +629,148 @@ internal sealed class Av1BlockDecoder : IDisposable int sourceIndex = ((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) + sourceOrigin.X + (sourceColumnQ4 >> 4); - Span destination = referenceIndex == 0 - ? blockReconstructionBuffer[reconstructionStride..] - : secondPrediction; + if (useCompoundIntermediates) + { + Span destination = referenceIndex == 0 + ? firstCompoundPrediction + : highBitDepthSecondPrediction; - Av1InterPredictor.Predict( - source, - sourceStride, - sourceIndex, - destination, - destinationStride, - predictionWidth, - predictionHeight, - modeInfo.InterpolationFilters[1], - modeInfo.InterpolationFilters[0], - horizontalPhase, - verticalPhase, - predictionScratch); + Av1InterPredictor.PredictCompound( + source, + sourceStride, + sourceIndex, + destination, + predictionWidth, + predictionWidth, + predictionHeight, + modeInfo.InterpolationFilters[1], + modeInfo.InterpolationFilters[0], + horizontalPhase, + verticalPhase, + predictionScratch); + } + else + { + Span 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 (highBitDepth) + if (useCompoundIntermediates) + { + Span destination = blockReconstructionBuffer[reconstructionStride..]; + ReadOnlySpan 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 destination = MemoryMarshal.Cast( highBitDepthBlockReconstructionBuffer[reconstructionStride..]); @@ -1073,6 +1257,202 @@ internal sealed class Av1BlockDecoder : IDisposable } } + /// + /// Reconstructs a subsampled chroma block assembled from multiple neighboring luma inter blocks. + /// + private bool TryPredictSub8x8Chroma( + ref Av1PartitionInfo partitionInfo, + Point modeInfoPosition, + Av1BlockSize blockSize, + int plane, + int subX, + int subY, + Point pixelPosition, + int predictionWidth, + int predictionHeight, + Span blockReconstructionBuffer, + Span highBitDepthBlockReconstructionBuffer, + int reconstructionStride, + Span 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 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 scaledDestination = default; + Span scaledHighBitDepthDestination = default; + if (this.frameBuffer.BytesPerSample == 2) + { + scaledHighBitDepthDestination = MemoryMarshal.Cast( + 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 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 destination = MemoryMarshal.Cast( + 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 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; + } + /// /// Resolves one canonical retained reference frame. /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs index 55d96115d..18c73a78b 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs @@ -636,6 +636,8 @@ public class Av1CompoundBlockDecoderTests const int frameSize = 32; const int blockOrigin = 8; const int blockSize = 8; + const int compoundRoundBits = 4; + const int compoundRoundOffset = (1 << 12) + (1 << 11); ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth, frameSize); ObuFrameHeader frameHeader = CreateFrameHeader(frameSize); frameHeader.GetReferenceFrameIndices()[0] = 0; @@ -667,8 +669,6 @@ public class Av1CompoundBlockDecoderTests Av1FrameBuffer firstReference = referenceFrames.Resolve(0)!.FrameBuffer; Av1FrameBuffer secondReference = referenceFrames.Resolve(1)!.FrameBuffer; - byte[] firstBytePrediction = new byte[blockSize * blockSize]; - byte[] secondBytePrediction = new byte[blockSize * blockSize]; ushort[] firstHighBitDepthPrediction = new ushort[blockSize * blockSize]; ushort[] secondHighBitDepthPrediction = new ushort[blockSize * blockSize]; short[] firstScratch = new short[Av1InterPredictor.WarpedScratchLength]; @@ -690,13 +690,13 @@ public class Av1CompoundBlockDecoderTests out int secondStride, out Point secondOrigin); - Av1InterPredictor.PredictWarpedScalar( + Av1InterPredictor.PredictWarpedCompoundScalar( firstSource, firstStride, firstOrigin, frameSize, frameSize, - firstBytePrediction, + firstHighBitDepthPrediction, blockSize, blockPosition, blockSize, @@ -706,13 +706,13 @@ public class Av1CompoundBlockDecoderTests globalMotionParameters, firstScratch); - Av1InterPredictor.PredictWarpedScalar( + Av1InterPredictor.PredictWarpedCompoundScalar( secondSource, secondStride, secondOrigin, frameSize, frameSize, - secondBytePrediction, + secondHighBitDepthPrediction, blockSize, blockPosition, blockSize, @@ -822,8 +822,15 @@ public class Av1CompoundBlockDecoderTests int predictionIndex = (row * blockSize) + column; if (bitDepth == Av1BitDepth.EightBit) { - byte expected = (byte)((firstBytePrediction[predictionIndex] + - secondBytePrediction[predictionIndex] + 1) >> 1); + // Libaom truncates the equal average before removing the Q4 compound bias, then performs the + // 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 samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(blockOrigin + row); Assert.Equal(expected, samples[blockOrigin + column]); diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterPredictorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterPredictorTests.cs index 0659680d3..27a59f5ef 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterPredictorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterPredictorTests.cs @@ -101,6 +101,13 @@ public class Av1InterPredictorTests public void HighBitDepthPredictionMatchesLibaomOracleAcrossIntrinsicWidths() => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthPredictions, PredictorConfigurations); + /// + /// Verifies that SIMD compound intermediates retain scalar-equivalent values and untouched destination padding. + /// + [Fact] + public void CompoundPredictionMatchesScalarAcrossIntrinsicWidths() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateCompoundPredictions, PredictorConfigurations); + /// /// Applies every byte prediction scenario to the SIMD-first and explicitly scalar entry points. /// @@ -165,12 +172,62 @@ public class Av1InterPredictorTests } } + /// + /// Applies every byte prediction scenario to the SIMD-first and scalar compound-intermediate entry points. + /// + 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"); + } + } + /// /// Creates the named operation matrix covering copy, each one-dimensional direction, separable filtering, reduced kernels, and vector tails. /// /// The prediction scenarios. 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("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), diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs index 27d3293b2..b2e1a1709 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs @@ -155,6 +155,37 @@ public class Av1ReconstructionConformanceTests /// private const int AverageCompoundFixtureFrameCount = 19; + /// + /// The displayed width of the official libaom motion-vector sequence. + /// + private const int OfficialMotionVectorFixtureWidth = 352; + + /// + /// The displayed height of the official libaom motion-vector sequence. + /// + private const int OfficialMotionVectorFixtureHeight = 288; + + /// + /// The number of shown frames in the official libaom motion-vector sequence. + /// + private const int OfficialMotionVectorFixtureFrameCount = 4; + + /// + /// The bit mask containing every single-reference and compound inter prediction mode. + /// + private const int RequiredInterModeCoverage = + (1 << ((int)Av1PredictionMode.InterModeEnd - (int)Av1PredictionMode.InterModeStart)) - 1; + + /// + /// The bit mask containing every simple, OBMC, and locally warped motion mode. + /// + private const int RequiredMotionModeCoverage = (1 << 3) - 1; + + /// + /// The bit mask containing every regular, smooth, and sharp vertical/horizontal filter pair. + /// + private const int RequiredSwitchableFilterPairCoverage = (1 << 9) - 1; + /// /// The coverage bit representing distance-weighted compound prediction. /// @@ -826,6 +857,144 @@ public class Av1ReconstructionConformanceTests fixtureSize: 256, visibleFrameCount: 2); + /// + /// 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. + /// + [Fact] + public void DecodeOfficialMotionVectorSequenceMatchesPinnedLibaomReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateOfficialMotionVectorFixtureWithDefaultConfiguration, + ReconstructionConfigurations); + + /// + /// Verifies the official motion-vector conformance sequence through constrained tracked allocation. + /// + [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)); + } + + /// + /// Runs the official motion-vector fixture with the default decoder configuration. + /// + private static void ValidateOfficialMotionVectorFixtureWithDefaultConfiguration() + => ValidateOfficialMotionVectorFixture(Configuration.Default); + + /// + /// 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. + /// + private static void ValidateOfficialMotionVectorFixture(Configuration configuration) + { + byte[] ivf = TestFile.Create(TestImages.Heif.Av1OfficialMotionVectorSequence).Bytes; + byte[] nativeReference = TestFile.Create(TestImages.Heif.Av1OfficialMotionVectorSequenceNativeReference).Bytes; + ReadOnlySpan y4mFileHeader = "YUV4MPEG2 W352 H288 F30:1 Ip C420jpeg\n"u8; + ReadOnlySpan 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 decodedFrame = decoder.DecodeSequenceFrame( + ivf.AsSpan(ivfOffset, payloadLength), + null, + null); + + using ImageFrame 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 frameBuffer = Assert.IsType>(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(decoder.SequenceHeader); + Av1FrameInfo frameInfo = Assert.IsType(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); + } + /// /// Verifies one complete inter-prediction sequence with a separately tracked constrained allocator. /// @@ -3032,9 +3201,12 @@ public class Av1ReconstructionConformanceTests { if (expected != actual) { + ObuSequenceHeader sequenceHeader = Assert.IsType(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(decoder.FrameInfo); - int modeInfoColumn = x >> Av1Constants.ModeInfoSizeLog2; - int modeInfoRow = y >> Av1Constants.ModeInfoSizeLog2; + int modeInfoColumn = (x << subsamplingX) >> Av1Constants.ModeInfoSizeLog2; + int modeInfoRow = (y << subsamplingY) >> Av1Constants.ModeInfoSizeLog2; Av1BlockModeInfo modeInfo = frameInfo.GetModeInfoAt(new Point(modeInfoColumn, modeInfoRow)); int blockColumn = modeInfoColumn; 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}. " + $"Total unequal samples={mismatchCount}:{mismatchDescription}. " + $"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)}, " + $"palette-size={modeInfo.GetPaletteSize(plane)}, transforms={modeInfo.GetTransformUnitCount(plane)}, " + $"transform={containingTransform.Size}/{containingTransform.Type}/coded={containingTransform.CodeBlockFlag} " diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index cd78e88e3..b24feddf7 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/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 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 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 Av1Cdef8BitReference = "Heif/Av1/Conformance/libaom-cdef-kodim23-8b-libaom.yuv"; public const string Av1Cdef8BitAvif = "Heif/Av1/Conformance/libavif-cdef-kodim23-8b.avif"; diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-05-mv-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-05-mv-libaom.y4m new file mode 100644 index 000000000..487c5e08c --- /dev/null +++ b/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 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-05-mv.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-05-mv.ivf new file mode 100644 index 000000000..952608615 --- /dev/null +++ b/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