Browse Source

Merge e1a86f9def into adb982081a

pull/2633/merge
Ynse Hoornenborg 1 day ago
committed by GitHub
parent
commit
7e6d1df23c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      .gitattributes
  2. 856
      HEIF_IMPLEMENTATION_PLAN.md
  3. 5
      ImageSharp.slnx
  4. 142
      THIRD-PARTY-NOTICES.TXT
  5. 2
      shared-infrastructure
  6. 53
      src/ImageSharp/Common/Helpers/DisposableDictionary.cs
  7. 51
      src/ImageSharp/Common/Helpers/DisposableList.cs
  8. 185
      src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
  9. 472
      src/ImageSharp/Common/Helpers/SimdUtils.Pack.cs
  10. 3
      src/ImageSharp/Configuration.cs
  11. 2
      src/ImageSharp/Formats/DecoderOptions.cs
  12. 25
      src/ImageSharp/Formats/Heif/Av1/Av1BitDepth.cs
  13. 19
      src/ImageSharp/Formats/Heif/Av1/Av1BitDepthExtensions.cs
  14. 314
      src/ImageSharp/Formats/Heif/Av1/Av1BitStreamReader.cs
  15. 266
      src/ImageSharp/Formats/Heif/Av1/Av1BitStreamWriter.cs
  16. 140
      src/ImageSharp/Formats/Heif/Av1/Av1BlockSize.cs
  17. 222
      src/ImageSharp/Formats/Heif/Av1/Av1BlockSizeExtensions.cs
  18. 775
      src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs
  19. 30
      src/ImageSharp/Formats/Heif/Av1/Av1ColorFormat.cs
  20. 318
      src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs
  21. 996
      src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs
  22. 654
      src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs
  23. 21
      src/ImageSharp/Formats/Heif/Av1/Av1LayerSelector.cs
  24. 97
      src/ImageSharp/Formats/Heif/Av1/Av1LayeredImageIndex.cs
  25. 309
      src/ImageSharp/Formats/Heif/Av1/Av1Math.cs
  26. 16
      src/ImageSharp/Formats/Heif/Av1/Av1OperatingPointSelector.cs
  27. 153
      src/ImageSharp/Formats/Heif/Av1/Av1PartitionType.cs
  28. 116
      src/ImageSharp/Formats/Heif/Av1/Av1PartitionTypeExtensions.cs
  29. 25
      src/ImageSharp/Formats/Heif/Av1/Av1Plane.cs
  30. 133
      src/ImageSharp/Formats/Heif/Av1/Color/Av1PlanarSampleBuffer.cs
  31. 778
      src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBuffer.cs
  32. 63
      src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBufferView.cs
  33. 203
      src/ImageSharp/Formats/Heif/Av1/Color/Av1YuvConverter.cs
  34. 2789
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs
  35. 420
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs
  36. 757
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContext.cs
  37. 155
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContexts.cs
  38. 319
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1MotionVectorContext.cs
  39. 501
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1NzMap.cs
  40. 1337
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolContextHelper.cs
  41. 1538
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs
  42. 674
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs
  43. 290
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolReader.cs
  44. 302
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs
  45. 22
      src/ImageSharp/Formats/Heif/Av1/IAv1TileReader.cs
  46. 19
      src/ImageSharp/Formats/Heif/Av1/IAv1TileWriter.cs
  47. 163
      src/ImageSharp/Formats/Heif/Av1/ModeDecision/Av1BlockGeometry.cs
  48. 1050
      src/ImageSharp/Formats/Heif/Av1/ModeDecision/Av1BlockGeometryFactory.cs
  49. 50
      src/ImageSharp/Formats/Heif/Av1/ModeDecision/Av1GeometryIndex.cs
  50. 500
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionParameters.cs
  51. 30
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionType.cs
  52. 415
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopy.cs
  53. 297
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVariationCandidates.cs
  54. 212
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVector.cs
  55. 25
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVectorPrecision.cs
  56. 1394
      src/ImageSharp/Formats/Heif/Av1/Motion/Av1ReferenceMotionVectors.cs
  57. 30
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuChromoSamplePosition.cs
  58. 116
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorConfig.cs
  59. 75
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorPrimaries.cs
  60. 40
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuConstraintDirectionalEnhancementFilterParameters.cs
  61. 32
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDecoderModelInfo.cs
  62. 25
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDeltaParameters.cs
  63. 292
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFilmGrainParameters.cs
  64. 357
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs
  65. 101
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameReferenceState.cs
  66. 40
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameSize.cs
  67. 30
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameType.cs
  68. 45
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuHeader.cs
  69. 77
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopFilterParameters.cs
  70. 20
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationItem.cs
  71. 40
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationParameters.cs
  72. 95
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs
  73. 40
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMetadataType.cs
  74. 46
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOperatingPoint.cs
  75. 53
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOrderHintInfo.cs
  76. 65
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuQuantizationParameters.cs
  77. 3110
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs
  78. 20
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReferenceMode.cs
  79. 30
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuRestorationType.cs
  80. 50
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSegmentationLevelFeature.cs
  81. 113
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSegmentationParameters.cs
  82. 229
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceHeader.cs
  83. 25
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceProfile.cs
  84. 109
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSkipModeParameters.cs
  85. 107
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTileGroupHeader.cs
  86. 36
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTimingInfo.cs
  87. 95
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTransferCharacteristics.cs
  88. 60
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuType.cs
  89. 935
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs
  90. 205
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs
  91. 35
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs
  92. 731
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs
  93. 34
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.ByteOutputOperator.cs
  94. 19
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.CopyFilterOperator.cs
  95. 50
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.Operator.cs
  96. 19
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.PrimaryAndSecondaryFilterOperator.cs
  97. 19
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.PrimaryFilterOperator.cs
  98. 19
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.SecondaryFilterOperator.cs
  99. 34
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.UInt16OutputOperator.cs
  100. 1694
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.cs

6
.gitattributes

@ -138,6 +138,12 @@
*.heic filter=lfs diff=lfs merge=lfs -text
*.hif filter=lfs diff=lfs merge=lfs -text
*.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
###############################################################################
# Handle ICC files by git lfs
###############################################################################

856
HEIF_IMPLEMENTATION_PLAN.md

@ -0,0 +1,856 @@
# AVIF and AV1 implementation plan
## Goal
Complete a production-quality, fully managed AV1 codec and its bounded AVIF/HEIF image container integration for ImageSharp. The finished work must decode and encode still images and bounded image sequences, preserve source precision, use ImageSharp memory ownership, and provide SIMD-first hot paths with one behaviorally identical scalar fallback.
This plan is the authoritative delivery checklist. A source file, unit test, build, self-roundtrip, or local implementation is not completion evidence by itself.
## Source authority
- AV1 codec syntax, tables, fixed-point arithmetic, prediction, transforms, entropy behavior, filters, encoder decisions, and lifecycle behavior must be ported and checked only against the current `main` branch of the official libaom checkout at `D:\GitHub\AOMediaCodec\aom`.
- Libaom is the sole external codec implementation source. Do not use HM, libheif, FFmpeg, GPAC, SVT-AV1, dav1d, libgav1, or any other codec implementation as an algorithm, arithmetic, output, or architecture reference.
- Existing ImageSharp and JPEG code is authoritative only for ImageSharp architecture, allocator ownership, SIMD dispatch, pixel conversion, and test API patterns. It is not an alternate AV1 algorithm source.
- Production code must not load, invoke, install, or fall back to a native codec.
- Existing independent container files may be used only as interoperability inputs. Native AV1 expected output must be generated by the current libaom `main` checkout, and no independent decoder output may substitute for it.
Reference checkout evidence on 2026-08-31:
- `D:\GitHub\AOMediaCodec\aom` is attached to `main`, clean, and aligned with `origin/main` after a fresh fetch.
- Both `HEAD` and `origin/main` resolved to `441c439b9916474cac15d2822af47a9ad70674a8`. This records the tree audited on that date; it is not a pin and must not prevent later work from updating to the then-current `main`.
## Status notation
- [x] Verified: the current behavior has exact evidence from the current libaom `main` tree and the evidence proves the production contract.
- [~] Locally implemented, checkpoint open: production source exists, but current-tree verification is missing or a known audit issue invalidates the checkpoint.
- [ ] Remaining: the production behavior is absent, incomplete, or has not reached its required implementation boundary.
## Current source reconciliation
Reconciled with the worktree on 2026-08-31.
- [~] The bounded container reader, still-image path, sequence parser, AV1 decoder, color pipeline, presentation pipeline, and broad AV1 test suite exist locally.
- [x] The inter-frame decoder has verified checkpoints through inter deblocking decisions and
reference/mode deltas.
- [~] Loop filtering, CDEF, super-resolution, restoration, film grain, layered presentation, alpha composition, and color conversion exist locally. Shared-source cleanup changed the current tree, so final production-path verification is open.
- [~] AV1 writer primitives, forward transforms, symbol encoding, and tile-writing source exist locally, but they are not connected to the public encoder.
- [ ] The public AV1 encoder is not implemented. HeifEncoderCore.Encode throws NotSupportedException when AV1 is selected.
- [x] Patented codec production code, registrations, tests, benchmarks, fixtures, reference outputs, and notices were manually deleted and committed by `78a74d448`.
- [x] Remaining task-created HM, HEVC, libheif, GPAC, Nokia, FFmpeg, Pillow HEIF, libavif-build, and libjpeg-build directories were traced to their creation commands in the recovered Codex session history and deleted on 2026-08-31. The user-provided repositories and all libaom-only source, build, and reference data were left untouched.
- [x] The PNG metadata-suppression fix and three HEIF/AV1 diagnostic-save call-site corrections passed the exact 34 net11.0 ARM CI cases and were committed with the single-reference checkpoint as `54bb6cbe59bd113058854a3ee31448cf61f462ca`. They are infrastructure evidence, not decoder or encoder completion evidence.
- [ ] The complete decoder and encoder release matrix is not complete.
## Immediate execution queue
Work must proceed in this order. Do not skip to a later item while an earlier checkpoint is open.
### 1. Finish and verify the AV1-only cleanup
- [x] Remove production types, registrations, constants, parser branches, properties, tests, benchmarks, fixtures, reference outputs, notices, and documentation for removed codec work.
- [x] Remove downloaded non-libaom reference source, tools, generated outputs, and local installations.
- [x] Retain the official current-main libaom checkout and libaom-only build artifacts required for AV1 verification.
- [x] Retain user-supplied AV1 fixtures and their recorded expected outputs.
- [x] Audit production source, tests, benchmarks, assets, project files, notices, and documentation for stale removed-code references.
- [x] The cleanup and cICP tree built in Release for net10.0 and net11.0 with restore disabled, build servers disabled, and one MSBuild node.
- [x] The exact 34 net11.0 ARM CI failures pass after the cICP correction, and the subsequent single-reference checkpoint set passes on net10.0 and net11.0.
- [x] Roslynk, scoped StyleCop, whitespace, and `git diff --check` accepted the cleanup and cICP checkpoint.
- [x] The cleanup and cICP evidence was recorded and committed with the single-reference checkpoint.
Historical cleanup evidence from 2026-08-30, retained with its limitation:
- Release source builds passed for net10.0 and net11.0 with zero warnings and zero errors. Both builds used `--no-restore`, `--disable-build-servers`, and one MSBuild node.
- The focused net10.0 HEIF decoder, encoder, metadata, sequence-parser, and AV1 reconstruction set passed 221 of 221 tests with zero failures and zero skips. It did not execute the net11.0 diagnostic-save path that later failed in CI.
- The Roslyn compiler and configured StyleCop analyzers accepted the changed production source. Roslynk's `open_solution` entry point was attempted separately but failed before returning a solution handle, so no Roslynk result is claimed.
- The tracked-source text and filename audit found no removed-code references outside the unchanged repository and shared-infrastructure `.gitattributes` patterns. A later history reconstruction found ignored task-created reference directories that this audit missed; those directories were deleted on 2026-08-31.
- `git diff --check` passed and neither `.gitattributes` file changed.
Current cICP failure correction evidence from 2026-08-31:
- The failure was not decoded HEIF metadata. `PngEncoderCore.WriteCicpChunk` ignored `PngChunkFilter.ExcludeAll`, so diagnostic PNG saves attempted to write a non-identity source matrix that PNG cannot represent.
- `PngEncoderCore` now honors the existing `SkipMetadata` contract for cICP, and the three affected HEIF/AV1 diagnostic saves explicitly use `PngEncoder { SkipMetadata = true }`. Actual comparisons and decoded-image metadata assertions remain unchanged.
- The direct embedded-ICC case and every row of the 12-case profile matrix passed: 13 of 13 net11.0 Release cases.
- The exact 34 cases reported by CI passed: 34 of 34 net11.0 Release cases, with zero failures and zero skips.
- Roslynk reported zero compiler errors after the fix, and `git diff --check` passed.
Recovered task-history evidence from 2026-08-31:
- The primary session beginning on 2026-08-24 was reopened from task ID `01a03239-831b-7831-84e7-7f6947279ccb`: 96,777 records, 295 turn contexts, 211 compactions, 190 user messages, 1,920 assistant messages, and 13,671 tool calls.
- The continuation beginning on 2026-08-27 was reopened from task ID `01a04314-f1c6-7133-b1bc-5c74a94dd714`: 61,129 records at the audit point, 166 turn contexts, 96 compactions, 223 user messages, 1,113 assistant messages, and 8,942 tool calls.
- The restored first session records the user selecting official AOM/libaom as the AV1 source after the ImageSharp discussion was inspected. It does not authorize another codec implementation as an AV1 source and does not authorize importing a patented codec.
- The restored tool calls identify the exact creation commands for the non-libaom source, tool, and output directories removed on 2026-08-31. No directory was selected for deletion from its name alone.
- The recovered Git sequence establishes that `78a74d448` removed the patented codec implementation and `92fa7a8ca` merged the later upstream ImageSharp changes. The current branch and worktree, not an older summary, remain authoritative.
### 2. Correct the single-reference inter-frame checkpoint
The checkpoint is complete through `c4b4e4e0386328dea574a884b6fa36c360ad5a9b`. It replaces frame-sized palette maps with fixed decoder-session scratch, reconstructs each superblock before reusing that scratch, and passes the ownership, documentation, full AV1 test, and Release source-build gates on both target frameworks.
- [x] Reconcile interpolation-filter syntax in `Av1TileReader` with current libaom `main`.
- Current libaom `av1_is_interp_needed` calls `is_nontrans_global_motion`, whose loop rejects only `TRANSLATION`. Identity GLOBALMV therefore omits switchable-filter symbols.
- Current `Av1TileReader` uses the same non-Translation classification. The existing Identity test leaves sentinel filter symbols unread, while the Translation test consumes them.
- No production change is required. The focused test describes only the syntax behavior it proves.
- [x] Reconcile both spatial single-reference extension loops in `Av1ReferenceMotionVectors` with current libaom `main`.
- Current libaom `setup_ref_mv_list` stops both loops at `MAX_MV_REF_CANDIDATES`, which is two. `MAX_REF_MV_STACK_SIZE`, which is eight, is the stack capacity used by the earlier direct and temporal candidate collection; it is not the stop condition for these two extension loops.
- Current `Av1ReferenceMotionVectors` uses the same two-entry stop condition and retains an eight-entry stack for earlier candidates and DRL selection.
- No production change is required. This remains spatial single-reference extension, not temporal extension.
- [x] Establish and enforce the contiguous frame-plane invariant used by `Av1FrameBuffer` and inter reconstruction.
- Every frame plane is allocated with `preferContiguosImageBuffers: true`, so a constrained allocator cannot split a representable padded plane into normal memory groups.
- `Av1FrameBuffer` now rejects an external frame geometry whose padded plane reaches the `int.MaxValue` fallback boundary before any allocation. This makes every direct `DangerousGetSingleSpan` call an enforced owner invariant rather than a memory-group accident.
- `ConstructorRequestsContiguousPaddedPlanes` proves that a plane larger than the allocator's group capacity is one group. `ConstructorRejectsPaddedPlaneThatCannotBeContiguous` proves that an unrepresentable plane is rejected before allocation.
- The production path performs no plane copy and no per-block, per-row, or per-scanline allocation.
- [x] Prove the real `Av1BlockDecoder.DecodeBlock` inter-reconstruction branch.
- Decode the progressive dependent-frame fixture through the complete public production path.
- Compare the final frame's native Y, Cb, and Cr planes exactly with current-main libaom output.
- Compare the final presented image through the established ImageSharp reference-image comparison API.
- Do not substitute an internal helper test, fake tile reader, non-zero assertion, custom pixel loop, or tolerant comparison.
- [x] Prove motion-field ownership and lifetime after the current reconstruction-timing change.
- Track initialization, retained-slot aliases, failure unwinding, presentation ownership, decoder-result ownership, and final disposal.
- Every allocator-owned object must be returned exactly once.
- [x] Correct stale documentation for the current worktree.
- Av1InterFrameModeInfoTests must describe the behavior it actually proves.
- Do not claim production reconstruction, constrained allocation, ownership, or reference-stack coverage unless the test executes that contract.
Checkpoint gate:
- [x] Default Identity-omission and Translation-consumption GLOBALMV syntax cases pass in the focused current-tree run.
- [x] Two-entry spatial single-reference extension passes; current-main source inspection confirms the separate eight-entry overall stack capacity and DRL access.
- [x] The exact dependent-frame native-plane comparison passes.
- [x] The established exact presentation comparison passes.
- [x] Normal, AVX-512-disabled, AVX-disabled, and scalar FeatureTestRunner configurations pass where supported.
- [x] Constrained allocation preserves the enforced single-group plane invariant without copying or per-block allocation.
- [x] Motion-field allocation tracking is balanced across success and failure on net10.0 and net11.0.
- [x] Release source builds pass for net10.0 and net11.0 with zero warnings and zero errors.
- [x] The complete AV1 namespace passes 8,732 of 8,732 tests on net10.0 and net11.0 with zero failures or skips.
- [x] Roslynk reports zero compiler errors; scoped analyzer inspection reports no diagnostics introduced by the current changes; `git diff --check` passes.
- [x] The completed checkpoint was committed as `54bb6cbe59bd113058854a3ee31448cf61f462ca` with author and committer `James Jackson-South <james_south@hotmail.com>`.
- [x] The palette-memory follow-up was committed as `c4b4e4e0386328dea574a884b6fa36c360ad5a9b` with author and committer `James Jackson-South <james_south@hotmail.com>`.
Verified single-reference checkpoint evidence on 2026-08-31:
- The current-main `aomdec` was rebuilt directly from `D:\GitHub\AOMediaCodec\aom` and identified itself as `3.15.0-13-g441c439b99`.
- Decoding the 72-byte progressive payload with `--all-layers`, one thread, and row multithreading disabled produced 2,178 YUV444 color samples. All samples in both layers match the first three planes of the stored YUV444-alpha reference exactly.
- `DecodeProgressiveSingleMatchesReference` executes the production decoder through FeatureTestRunner and compares the complete presented `Rgba32` image with `CompareToReferenceOutput(ImageComparer.Exact, provider)`. The redundant manual alpha loop was removed.
- `DecodeProgressiveSingleWithConstrainedAllocator` executes the same production reconstruction with a 1,024-byte allocator group capacity and verifies that every allocation is returned exactly once.
- `MotionFieldsFollowAliasesAndPresentationOwnership`, `MotionFieldAllocationFailureUnwindsTileReaderOwnership`, `DecodeProgressiveSingleTracksMotionFieldOwnership`, and the reference-store replacement, reset, and transfer tests cover initialization, aliases, presentation ownership, decoder-result ownership, failure unwinding, repeated disposal, and exactly-once final returns in the current worktree.
- The current worktree passes the four-case palette set, seven-case ownership set, and 29-case syntax, plane, and production reconstruction set on both target frameworks. The complete AV1 namespace passes 8,732 of 8,732 tests on net10.0 and net11.0 with zero failures or skips.
- Release source builds passed for net10.0 and net11.0 with zero warnings and zero errors.
- Roslynk reported zero compiler errors. The scoped changed-file analyzer inspection reported no StyleCop diagnostics attributable to this checkpoint; its only remaining match is the pre-existing xUnit cancellation warning in an unrelated `HeifDecoderTests` method.
- `git diff --check` passed, and neither `.gitattributes` file changed.
Exact verification commands, run directly in the foreground from `D:\GitHub\ynse01\ImageSharp`:
```powershell
$env:MSBUILDUSESERVER = '0'
$env:DOTNET_CLI_USE_MSBUILD_SERVER = '0'
$env:DOTNET_CLI_HOME = 'D:\GitHub\ynse01\ImageSharp\.dotnet'
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1'
$env:DOTNET_CLI_TELEMETRY_OPTOUT = '1'
$env:DOTNET_DbgEnableMiniDump = '0'
$env:COMPlus_DbgEnableMiniDump = '0'
$env:DOTNET_EnableCrashReport = '0'
$env:COMPlus_EnableCrashReport = '0'
$heifCheckpointFilter = 'FullyQualifiedName~Av1InterFrameModeInfoTests.ReadInterFrameModeInfoReadsInterpolationFilters|FullyQualifiedName~Av1InterFrameModeInfoTests.IdentityGlobalMotionOmitsInterpolationFilters|FullyQualifiedName~Av1ReferenceMotionVectorsTests.BuildReversesOppositeDirectionExtensionCandidate|FullyQualifiedName~Av1FrameBufferTests|FullyQualifiedName~Av1ReferenceFrameStoreTests.MotionFieldsFollowAliasesAndPresentationOwnership|FullyQualifiedName~Av1ReferenceFrameStoreTests.MotionFieldAllocationFailureUnwindsTileReaderOwnership|FullyQualifiedName~Av1ReferenceFrameStoreTests.PartialReplacementPreservesSharedOwner|FullyQualifiedName~Av1ReferenceFrameStoreTests.FinalReplacementReleasesDisplacedOwner|FullyQualifiedName~Av1ReferenceFrameStoreTests.ResetReleasesUniqueOwnersAndClearsSlots|FullyQualifiedName~Av1ReferenceFrameStoreTests.TakeOutputTransfersPlanesAndReleasesOtherReferences|FullyQualifiedName~Av1ReconstructionConformanceTests.DecodeProgressiveSingleMatchesReference|FullyQualifiedName~Av1ReconstructionConformanceTests.DecodeProgressiveSingleWithConstrainedAllocator|FullyQualifiedName~Av1ReconstructionConformanceTests.DecodeProgressiveSingleTracksMotionFieldOwnership'
dotnet build src\ImageSharp\ImageSharp.csproj -c Release -f net10.0 --no-restore --disable-build-servers -m:1 --no-incremental --nologo --verbosity:minimal
dotnet build src\ImageSharp\ImageSharp.csproj -c Release -f net11.0 --no-restore --disable-build-servers -m:1 --no-incremental --nologo --verbosity:minimal
dotnet test tests\ImageSharp.Tests\ImageSharp.Tests.csproj -c Release -f net10.0 --no-restore --disable-build-servers -m:1 --filter $heifCheckpointFilter --logger 'console;verbosity=minimal'
dotnet test tests\ImageSharp.Tests\ImageSharp.Tests.csproj -c Release -f net11.0 --no-restore --disable-build-servers -m:1 --filter $heifCheckpointFilter --logger 'console;verbosity=minimal'
```
```powershell
$aomVcVars = 'C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Auxiliary\Build\vcvars64.bat'
$aomCmake = 'C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe'
$aomEnvironment = & cmd.exe /d /s /c "`"$aomVcVars`" >nul && set"
foreach ($aomEntry in $aomEnvironment)
{
$aomParts = $aomEntry -split '=', 2
if ($aomParts.Length -eq 2)
{
[Environment]::SetEnvironmentVariable($aomParts[0], $aomParts[1], 'Process')
}
}
& $aomCmake --build artifacts\reference\aom-generic --target aomdec --config Release --parallel 1
& 'artifacts\reference\aom-generic\aomdec.exe' --codec=av1 --rawvideo --all-layers --threads=1 --row-mt=0 --output='artifacts\reference\aom-generic\progressive-current-main-all-layers.yuv' 'tests\Images\Input\Heif\Av1\Conformance\libavif-progressive-draw-points-8b.bit'
```
### 3. Reverify downstream inter prediction in recorded order
The single-reference syntax, buffer, reconstruction, and ownership foundation is verified by `54bb6cbe59bd113058854a3ee31448cf61f462ca`. Reverify the existing downstream implementations in this exact order, treating each as locally implemented but unverified until its current-main evidence is recorded.
- [x] Compound reference selection, paired reference-MV derivation, and equal averaging.
- [x] Inter-intra prediction.
- [x] Distance-weighted compound prediction.
- [x] Wedge compound prediction.
- [x] Difference-weighted compound prediction.
- [x] OBMC.
- [x] Scaled-reference prediction.
- [x] Local warped prediction.
- [x] Non-translational global prediction.
- [x] Inter deblocking decisions and reference/mode deltas.
Verified equal-average compound checkpoint evidence on 2026-08-31:
- [x] Refreshed the clean official libaom `main` checkout and audited the observed revision
`441c439b9916474cac15d2822af47a9ad70674a8`. Reference selection and compound mode syntax match
`read_comp_reference_type` and `read_ref_frames` in `av1/decoder/decodemv.c`; contexts match
`av1/common/pred_common.c`; paired reference-MV construction and eight-entry extension match
`process_compound_ref_mv_candidate` and `setup_ref_mv_list` in `av1/common/mvref_common.c`.
- [x] Audited equal-average reconstruction against `av1/common/convolve.c` and
`av1/common/convolve.h`. Corrected the unscaled 10/12-bit translational path so both references
retain libaom's no-round compound intermediates until the sole final average and clipping step,
including the larger first-round shift required for 12-bit horizontal intermediates.
- [x] Added descending Vector512, Vector256, Vector128, and scalar high-bit-depth traversal to the
existing semantic compound-prediction operator families. No per-block, per-row, or per-scanline
allocation or copy was added.
- [x] Added FeatureTestRunner coverage for 10/12-bit copy, horizontal, vertical, and separable
subpixel prediction at widths 9, 17, 33, and 65, with an independent no-round bilinear oracle,
row-padding sentinels, and explicit scalar comparison.
- [x] Added a complete `Av1BlockDecoder.DecodeBlock` 10/12-bit half-sample regression whose expected
result comes from the scalar no-round pipeline. The selected vector differs by one sample from the
obsolete round-each-reference behavior, so the test proves the production branch selection.
- [x] Refreshed the official libaom `main` remote immediately before verification and decoded the
fixture's 5,465-byte AV1 `mdat` payload with current `aomdec`, one thread and row threading
disabled. All 19 frames decoded; the final 19,200 YUV444 samples have SHA-256
`E79D2F49C260B1AC9B1B9BBBB2D611126AFD3B241DA389EB9E7BD4EA0ED42080` and match the retained native
reference with zero differing samples.
- [x] The real 19-frame production sequence requires decoded equal-average compound blocks, compares
the final native Y, U, and V planes exactly, compares final RGBA presentation through ImageSharp's
established reference-output API, and repeats the complete decode with a 1,024-byte constrained
tracked allocator and exactly-once return checks.
- [x] The focused Release checkpoint set passes 31/31 on net10.0 and 31/31 on net11.0, with zero
failures or skips. Scoped analyzer and whitespace verification pass for every changed C# file,
Roslynk reports zero compiler errors and no diagnostics in the changed files, and `git diff --check`
passes. `.gitattributes` is unchanged.
- [x] The completed checkpoint was committed as `4075a0844836e863a93cb2e2f3ca42d202c7df1b`
with author and committer `James Jackson-South <james_south@hotmail.com>`.
Verified inter-intra checkpoint evidence on 2026-08-31:
- [x] Audited syntax against current libaom `av1/decoder/decodemv.c` and
`av1/common/blockd.h`. ImageSharp applies the same sequence enable, skip-mode, block-size, and
single-reference gates, reads the same four-mode CDF, and reads wedge syntax only within libaom's
wedge-supported `BLOCK_8X8` through `BLOCK_32X32` range.
- [x] Audited reconstruction against `ii_weights1d`, `ii_size_scales`,
`build_smooth_interintra_mask`, and `combine_interintra` in current
`av1/common/reconinter.c`. The ImageSharp weights, plane-size scaling, smooth-mask direction,
complemented destination orientation, wedge sign, subsampling, and final 6-bit blend match. No
production change was required.
- [x] The mask tests cover all four inter-intra modes, complemented orientation, row-padding
sentinels, and the 32-wide curve. FeatureTestRunner covers byte and high-bit-depth selectable
blending under SIMD and scalar dispatch, and complete `Av1BlockDecoder.DecodeBlock` tests execute
smooth inter-intra reconstruction at 8, 10, and 12 bits.
- [x] Extracted the fixture's 5,327-byte AV1 `mdat` payload and decoded it with the refreshed current
libaom `aomdec`, using one thread with row threading disabled. All 19 frames decoded. The final
19,200 YUV444 samples have SHA-256
`E8B776C2751DC30CA838931A4B74535FC6E681179568A1278747A38CFF2E5BFA` and match the retained
native reference with zero differing samples.
- [x] The real production sequence requires both smooth and wedge inter-intra blocks, compares the
final native Y, Cb, and Cr planes exactly, and compares final RGBA presentation through
ImageSharp's established reference-output API. Its constrained 1,024-byte tracked-allocator run
proves motion-field allocation and exactly one return for every allocation.
- [x] The focused Release checkpoint set passes 50/50 on net10.0 and 50/50 on net11.0, with zero
failures or skips. Scoped analyzer and whitespace verification pass for both changed C# files.
Roslynk reports zero compiler errors and no diagnostics in the changed files, `git diff --check`
passes, and `.gitattributes` is unchanged.
- [x] The completed checkpoint was committed as `18b1c881271a3494489ca6f410ab140544902e2d`
with author and committer `James Jackson-South <james_south@hotmail.com>`.
Verified distance-weighted compound checkpoint evidence on 2026-08-31:
- [x] Audited reference-distance quantization against `quant_dist_weight` and
`quant_dist_lookup_table` in current libaom `av1/common/common_data.h`, and audited order-hint
distance selection and forward/backward reference assignment against
`av1_dist_wtd_comp_weight_assign` in `av1/common/reconinter.c`.
- [x] Audited reconstruction against current libaom `av1/common/convolve.c`. Corrected the production
10/12-bit subpixel path, which incorrectly finalized its two no-round compound intermediates with an
equal average instead of the signaled distance weights. The fixed path applies libaom's 4-bit weighted
shift before bias removal, final rounding, and clipping.
- [x] Added descending Vector512, Vector256, Vector128, and scalar traversal to the existing semantic
distance-weighted intermediate predictor family. Unsigned widening preserves the biased 12-bit
intermediate range. No per-block, per-row, or per-scanline allocation or copy was added.
- [x] Added FeatureTestRunner coverage for every current-libaom distance-weight class in both reference
orders, and for 10/12-bit copy, horizontal, vertical, and separable subpixel prediction at widths 9,
17, 33, and 65, with an independent no-round oracle and row-padding sentinels.
- [x] Added a complete `Av1BlockDecoder.DecodeBlock` 10/12-bit half-sample regression that selects the
13:3 distance weights through real order hints. Its first reconstructed sample differs from the old
equal-average result, so the test proves the corrected production branch is executed.
- [x] Extracted the fixture's 5,372-byte AV1 `mdat` payload and decoded it with the refreshed current
libaom `aomdec`, using one thread with row threading disabled. All 19 frames decoded. The final 19,200
YUV444 samples have SHA-256
`E8CAA650F1571C5B9CACAF8C06E1DDF5F5D2ED35F65F1C34377076C573425899` and match the retained native
reference with zero differing samples.
- [x] The real 19-frame production sequence requires decoded distance-weighted compound blocks, compares
the final native Y, Cb, and Cr planes exactly, compares final RGBA presentation through ImageSharp's
established reference-output API, and repeats the complete decode with a 1,024-byte constrained
tracked allocator and exactly-once return checks.
- [x] The focused Release checkpoint set passes 44/44 on net10.0 and 44/44 on net11.0, with zero failures
or skips. Scoped analyzer and whitespace verification pass for every changed C# file. Roslynk reports
zero compiler errors and no diagnostics in the changed files, `git diff --check` passes, and
`.gitattributes` is unchanged.
- [x] The completed checkpoint was committed as `7e2de7a2c25852acc374b17936a1a644464f77f3`
with author and committer `James Jackson-South <james_south@hotmail.com>`.
Verified wedge compound checkpoint evidence on 2026-08-31:
- [x] Audited mask generation against current libaom `tools/gen_wedge_masks_data.py` and
`av1/common/reconinter.c`, including the master prototypes, direction transforms, block-size
codebooks, sign flips, offsets, and luma/chroma mask sampling. ImageSharp's generated masks match
those definitions; only stale “pinned” documentation required correction.
- [x] Audited reconstruction against current libaom `aom_dsp/blend_a64_mask.c`. The high-bit-depth
d16 path applies the Q6 mask to both no-round intermediates before bias removal, the sole final
rounding step, and clipping.
- [x] Corrected the production high-bit-depth intermediate eligibility gate, which admitted only
equal-average blocks and made the distance-weighted and wedge no-round finalizers unreachable.
Average, distance-weighted, and wedge subpixel blocks now retain both intermediates until their
signaled finalizer; difference-weighted blending remains excluded for its next ordered checkpoint.
- [x] Added high-bit-depth traversal to the existing semantic mask-blend predictor and readonly
operator family with descending Vector512, Vector256, Vector128, and scalar dispatch. Unsigned
widening preserves the biased 12-bit intermediate range. No per-block, per-row, or per-scanline
allocation or copy was added.
- [x] Extended FeatureTestRunner coverage with an independent Q6 mask oracle across 10/12-bit copy,
horizontal, vertical, and separable subpixel prediction, widths 9, 17, 33, and 65, all mask weights
from 0 through 64, and row-padding sentinels. A complete `Av1BlockDecoder.DecodeBlock` regression
verifies the current-libaom 8x8 wedge mask and the production no-round branch.
- [x] Extracted the fixture's 5,374-byte AV1 `mdat` payload and decoded it with refreshed current
libaom `aomdec`, using one thread with row threading disabled. All 19 frames decoded. The final
19,200 YUV444 samples have SHA-256
`E8CAA650F1571C5B9CACAF8C06E1DDF5F5D2ED35F65F1C34377076C573425899` and match the retained native
reference with zero differing samples.
- [x] The real 19-frame production sequence requires both wedge-mask orientations, compares final
native Y, Cb, and Cr planes exactly, compares final RGBA presentation through ImageSharp's
established reference-output API, and repeats the complete decode with a 1,024-byte constrained
tracked allocator and exactly-once return checks.
- [x] The focused Release checkpoint set passes 35/35 on net10.0 and 35/35 on net11.0, with zero
failures or skips. Scoped analyzer and whitespace verification pass for every changed C# file.
Roslynk reports zero compiler errors, `git diff --check` passes, and `.gitattributes` is unchanged.
- [x] The completed checkpoint was committed as `9883a24dc319e16b471f68be632d4f62f2c1cd5e`
with author and committer `James Jackson-South <james_south@hotmail.com>`.
Verified difference-weighted compound checkpoint evidence on 2026-08-31:
- [x] Audited syntax against current libaom `av1/decoder/decodemv.c`. ImageSharp applies the same
masked-compound enable and block-size gates, selects difference-weighted compound directly when wedge
is unavailable, and reads the same one-bit type-38 mask orientation.
- [x] Audited mask generation and reconstruction against current libaom `av1/common/reconinter.c` and
`aom_dsp/blend_a64_mask.c`. The d16 path rounds the absolute intermediate difference by the
convolution and bit-depth shift, scales it by 1/16, adds the type-38 base, clamps or inverts the mask,
and then blends the original no-round intermediates before final rounding and clipping. Chroma reuses
the luma-derived mask through rounded subsampling.
- [x] Corrected the production 10/12-bit subpixel eligibility gate, which previously rounded both
references before difference-mask construction and blending. Difference-weighted blocks now use the
existing semantic intermediate mask-builder and mask-blend predictor/operator families through the
sole final rounding step. No new operator family, per-block allocation, or copy was introduced.
- [x] Renamed the stale “pinned formula” test and extended FeatureTestRunner's independent oracle across
current-libaom regular and d16 mask arithmetic, both mask orientations, 8/10/12-bit samples, widths
that cross every Vector512, Vector256, Vector128, and scalar boundary, subpixel phases, and row-padding
sentinels.
- [x] Added a complete `Av1BlockDecoder.DecodeBlock` regression for 10/12-bit half-sample prediction
and both type-38 orientations. Its expected mask and reconstruction are calculated directly from the
current-libaom equations, independently of the production mask builder and finalizer.
- [x] Extracted the fixture's 5,358-byte AV1 `mdat` payload and decoded it with refreshed current
libaom `aomdec`, using one thread with row threading disabled. All 19 frames decoded. The final
19,200 YUV444 samples have SHA-256
`E8CAA650F1571C5B9CACAF8C06E1DDF5F5D2ED35F65F1C34377076C573425899` and match the retained native
reference with zero differing samples.
- [x] The real 19-frame production sequence requires both difference-mask orientations, compares final
native Y, Cb, and Cr planes exactly, compares final RGBA presentation through ImageSharp's established
reference-output API, and repeats the complete decode with a 1,024-byte constrained tracked allocator
and exactly-once return checks.
- [x] The focused Release checkpoint set passes 37/37 on net10.0 and 37/37 on net11.0, with zero
failures or skips. Scoped analyzer and whitespace verification pass for every changed C# file.
Roslynk reports zero compiler errors, `git diff --check` passes, and `.gitattributes` is unchanged.
- [x] The completed checkpoint was committed as `fb4c64474e1ced4067a42731384f3b5ad4212a2f`
with author and committer `James Jackson-South <james_south@hotmail.com>`.
Verified OBMC checkpoint evidence on 2026-08-31:
- [x] Audited motion-mode syntax against current libaom `av1/decoder/decodemv.c`,
`av1/common/blockd.h`, `av1/common/reconinter.c`, `av1/common/obmc.h`, and
`av1/common/reconinter_template.inc`. ImageSharp applies the same switchable-mode, skip,
single-reference, inter-intra, minimum-size, overlappable-neighbor, fixed-global-motion, scaled
reference, and projection-sample gates and reads the matching binary or three-way CDF.
- [x] Audited above and left neighbor traversal, 4x4 pairing, neighbor caps, chroma suppression,
prediction rectangles, interpolation filters, first-reference selection, mask tables, and blend
order against current libaom. The existing semantic mask-blend predictor remains the correct
SIMD-first traversal; no OBMC-specific operator family, allocation, or copy was introduced.
- [x] Corrected the unscaled neighbor far-edge UMV clamp. After converting libaom's neighbor-relative
motion-vector limits to an absolute source coordinate, the prediction extent cancels from the
right and bottom limits; the previous code counted it twice.
- [x] Extracted the fixture's 5,387-byte AV1 `mdat` payload at AVIF offset 1,065 and decoded it with
refreshed current libaom `aomdec`, using one thread with row threading disabled. All 19 frames
decoded. The final 19,200 YUV444 samples have SHA-256
`E8CAA650F1571C5B9CACAF8C06E1DDF5F5D2ED35F65F1C34377076C573425899` and match the retained native
reference with zero differing samples.
- [x] The production sequence asserts decoded OBMC mode state, compares final native Y, Cb, and Cr
planes exactly, compares final RGBA presentation through ImageSharp's established reference-output
API under normal and scalar FeatureTestRunner dispatch, and repeats reconstruction with a 1,024-byte
constrained tracked allocator. Direct `DecodeBlock` tests cover above-then-left blending at
8/10/12-bit and 4:2:0 and 4:2:2 chroma geometry.
- [x] Renamed the stale pinned-reference test and its established reference-output PNG together. The
PNG SHA-256 remains
`D2CB388C9092EF17C4F0382C0150DD30D6F9D0EE247FF45AB5D7D4D312CEB23C`; only its contract-derived
filename changed.
- [x] The focused Release checkpoint set passes 18/18 on net10.0 and 18/18 on net11.0, with zero
failures or skips. Scoped analyzer and whitespace verification pass for every changed C# file.
Roslynk reports zero compiler errors, `git diff --check` passes, and `.gitattributes` is unchanged.
- [x] The completed checkpoint was committed as `7e7e3cbe6438d63926b31d966795d2652e221939`
with author and committer `James Jackson-South <james_south@hotmail.com>`.
Verified scaled-reference checkpoint evidence on 2026-08-31:
- [x] Audited reference-size validation and variable-scale coordinates, filters, edge extension, convolution
rounding, and compound intermediates against current libaom `av1/common/scale.c`,
`av1/decoder/decodeframe.c`, and `av1/common/convolve.c`. The frame boundary accepts the same
half-to-sixteen-times dimension range and requires at least one compatible selected reference.
- [x] Corrected the production scaled-compound branch. It previously rounded each scaled reference into
native pixels before blending; current libaom retains both `CONV_BUF_TYPE` values with
`COMPOUND_ROUND1_BITS` equal to seven and performs one final rounding after the selected compound blend.
- [x] Kept native-pixel and compound output in the existing `Av1ScaledInterPredictor` traversal with
semantic `NativeOperator` and `CompoundOperator` output contracts. The closed generic traversal shares
variable-phase arithmetic across byte and ushort sources, dispatches Vector512, Vector256, Vector128,
then scalar, and adds no per-block allocation or copy.
- [x] Added independent FeatureTestRunner oracles for native and no-round compound output across 8, 10,
and 12 bits, variable phases, all interpolation families, reduced kernels, vector tails, and destination
padding. A complete `Av1BlockDecoder.DecodeBlock()` regression covers scaled compound prediction across
all, AVX-512-disabled, AVX-disabled, and scalar configurations and proves the vector differs from an
incorrectly early-rounded blend.
- [x] Decoded the 2,195-byte layered payload with refreshed current libaom `aomdec`, using one thread,
row threading disabled, all layers selected, and raw 8-bit output. The 40x40 YUV444 base and 80x80
YUV444 dependent frames total 24,000 samples with SHA-256
`DD219E41B52C6C9343A92CD0A2D451DF57B73B25F10124811675B4CB2F8D666F`; both match their retained
native references with zero differing samples.
- [x] The production tests compare both native frames exactly, compare selected-layer and final RGBA
presentation through ImageSharp's established reference-output API, and repeat both paths with a
1,024-byte constrained tracked allocator whose allocations have balanced exactly-once returns.
- [x] Renamed the two stale pinned-reference tests and their contract-derived PNGs together. Their Git blob
identifiers remain unchanged, and their SHA-256 values remain
`DC4C6DBE6BD92C5FCE1E3E23700AFA603EF04ED02EDD336213EBBA1E3BD84BA0` and
`678C5E5D4650EA6F0C590302E7DB9E3C6608851BC577453DA4A6837BDB4D3AF3`.
- [x] The focused Release checkpoint set passes 10/10 on net10.0 and 10/10 on net11.0, with zero failures
or skips. Scoped analyzer and whitespace verification pass for every changed C# file. Roslynk reports
zero compiler errors, `git diff --check` passes, and `.gitattributes` is unchanged.
- [x] The completed checkpoint was committed as `658a9cd1b6e22806decbae923da8800bca03a09e`
with author and committer `James Jackson-South <james_south@hotmail.com>`.
Verified local warped-prediction checkpoint evidence on 2026-08-31:
- [x] Refreshed the clean official libaom `main` checkout and audited the observed revision
`441c439b9916474cac15d2822af47a9ad70674a8`. Motion-mode eligibility and CDF selection match
`read_motion_mode` in `av1/decoder/decodemv.c`; above, left, top-left, and top-right spatial projection
samples and threshold selection match `findSamples` and `selectSamples` in
`av1/common/mvref_common.c`; affine fitting, shear reduction, phase derivation, filters, rounding,
clipping, and invalid-model fallback match `av1/common/warped_motion.c` and
`av1/common/reconinter.c`.
- [x] Mechanically compared all 1,544 ImageSharp and independent-test warped-filter coefficients against
current libaom's `av1_warped_filter`; both comparisons have zero differences. The separate scalar test
transcription covers 8-, 10-, and 12-bit luma and subsampled-chroma coordinates, tail widths, destination
stride preservation, libaom's 12-bit round adjustment, and AVX-512, AVX, 128-bit, and scalar dispatch
through `FeatureTestRunner`.
- [x] Extracted the fixture's exact 2,310-byte AV1 `mdat` payload at AVIF offset 997. Its SHA-256 is
`644D04FE1D1A32BB7A3856AD7EB49CF1EFDE0AC845E55BEAC4170F72353F2391`. Current official
libaom decoded both 256x256 YUV444 frames with one thread, row threading disabled, and all layers enabled.
The complete Y4M SHA-256 is
`8FDC5D46014F5E5A7455A83643AB6F0DA66FC5A984E72A43F8C75BAD8271C299`; the final
frame's 196,608 native samples have SHA-256
`47B2AB39BF3B9DA15C1EC59840F964DFDF227760947F6E1295FB38A84555F75C` and match the
retained native reference with zero differences.
- [x] The real two-frame fixture exercises `Av1BlockDecoder.DecodeBlock()`, requires decoded
`WARPED_CAUSAL` state and the expected multi-sample affine model, compares final native Y, U, and V
planes exactly, compares the retained final presentation through ImageSharp's established reference-output
API, and passes through intrinsic and scalar dispatch. The 1,024-byte constrained tracked-allocator path
passes with motion-field allocations present and balanced exactly-once returns.
- [x] Renamed the stale pinned-reference test and its contract-derived PNG together without changing the PNG
bytes. Its SHA-256 remains
`4490D62FB6679378E92CACA48427359091AD2106BE49FC1A3848F78BE03BEEB1`.
- [x] The focused Release checkpoint set passes 4/4 on net10.0 and 4/4 on net11.0, with zero failures or
skips. Scoped analyzer verification passes for both changed C# files. Roslynk reports zero compiler errors,
`git diff --check` passes, and `.gitattributes` is unchanged.
- [x] The completed checkpoint was committed as `27a522424fe7aaea25078e705d71a501da110727`
with author and committer `James Jackson-South <james_south@hotmail.com>`.
Verified non-translational global-prediction checkpoint evidence on 2026-08-31:
- [x] Audited global-motion syntax, coefficient decoding, previous-reference recentering, shear validation,
motion-vector projection, and warped-prediction eligibility against current official libaom `main` at the
observed revision `441c439b9916474cac15d2822af47a9ad70674a8`. The implementation matches
`read_global_motion_params`, `read_global_motion_model`, `gm_get_motion_vector`, `is_global_mv_block`,
and the WARP_PRED selection in `av1/common/reconinter.c`.
- [x] Corrected high-bit-depth compound warped/global prediction to retain both references in libaom's
unsigned no-round compound domain. Current `get_conv_params_no_round`, `av1_warp_plane`, and
`av1_highbd_warp_affine_c` require the 12-bit first-round adjustment while retaining a seven-bit second
round; native clipping now occurs only after the compound blend.
- [x] The independent scalar libaom transcription validates native and no-round compound output for byte,
8-bit, 10-bit, and 12-bit sources, including tail widths and destination-stride preservation. All cases pass
through AVX-512, AVX, 128-bit, and scalar dispatch with `FeatureTestRunner`. Direct
`Av1BlockDecoder.DecodeBlock()` coverage validates `GLOBAL_GLOBALMV` compound reconstruction at all
supported bit depths.
- [x] Extracted the fixture's exact 38,475-byte AV1 `mdat` payload at AVIF offset 997. Its SHA-256 is
`6AC7EC9984B1FF5C00403D7E3858441E9CEE75128F7414101D06DEEE59A351D0`. Current
official libaom decoded both 256x256 YUV444 frames with one thread, row threading disabled, and all layers
enabled. The complete Y4M SHA-256 is
`84754DE0B9FABC4F3F8F344C848183EC17B625BFD87E4519C3D8AD7DEFD20F2C`; the final
frame's 196,608 native samples have SHA-256
`FEC89E2DE7496980389806B194425042F3800C7BAA817249D1A51D44A2B37A8E` and match the
retained native reference with zero differences.
- [x] The real two-frame fixture exercises the production decoder, requires decoded non-translational global
motion, compares final native Y, U, and V planes exactly, compares the retained presentation through
ImageSharp's established reference-output API, and passes the constrained tracked-allocator path.
- [x] Renamed the stale pinned-reference test and its contract-derived PNG together without changing the PNG
bytes. Its SHA-256 remains
`F7D27ABF79450DFA311F72106FD1DA80997EABC0937F2F5578EF627119FF83B0`, and Git
attributes select the LFS filter and diff driver.
- [x] The focused Release checkpoint set passes 11/11 on net10.0 and 11/11 on net11.0, with zero failures or
skips. Scoped analyzer verification passes for all six changed C# files. Roslynk reports zero compiler
errors, `git diff --check` passes, and `.gitattributes` is unchanged.
- [x] The completed checkpoint was committed as `25295683d39a2336e9b98484c9fd54f33107ea66`
with author and committer `James Jackson-South <james_south@hotmail.com>`.
Verified inter-deblocking checkpoint evidence on 2026-08-31:
- [x] Audited frame-level loop-filter syntax and primary-reference inheritance against
`setup_loopfilter` in current `av1/decoder/decodeframe.c`; per-superblock delta-LF parsing and
prediction against `read_delta_q_params` in `av1/decoder/decodemv.c`; and default reference/mode
deltas against `av1/common/entropymode.c` at observed current-main revision
`441c439b9916474cac15d2822af47a9ad70674a8`.
- [x] Audited filter-level derivation, segmentation adjustment, reference scaling, global/non-global
mode classes, skipped-transform prediction-unit decisions, transform-edge selection, kernel length,
sharpness limits, and vertical-then-horizontal traversal against `get_filter_level`,
`set_lpf_parameters`, `av1_filter_block_plane_vert`, `av1_filter_block_plane_horz`, and
`av1_thread_loop_filter_rows`. No production arithmetic change was required.
- [x] Added direct production `Av1LoopFilterDecoder.DecodeFrame()` coverage using adjacent skipped
16x8 inter blocks split into 8x8 transforms. An independent scalar oracle proves that internal
transform edges remain untouched and the prediction-unit edge uses current-libaom levels 17 for
LAST/GLOBALMV, 21 for LAST/NEWMV, and 22 for GOLDEN/GLOBALMV. Existing `FeatureTestRunner`
coverage continues to verify every filter width at 8, 10, and 12 bits under intrinsic and scalar
dispatch.
- [x] Current official libaom decoded the retained 20,750-byte 8-bit, 37,169-byte 10-bit, and
23,769-byte 12-bit elementary streams with one thread, row threading disabled, raw output, and their
native output depths. The generated native files match the retained references byte for byte. Their
output SHA-256 values are
`8DDE2EEC742C39F0579C29AE84CBA0FE01522A9008ADCB2CFFCCEC0295D18141`,
`9A59DD92A0C579F942ACCA8281EBD0465DC848BE200A4D2FF57EAFF589445F6C`, and
`EF712BE32AF7CF0A95C5C41BDCC51AFC05A4AB7C047383F5F65EDAD2BB986712`.
- [x] Reused the already current-main scaled-reference sequence as the real inter checkpoint. It
requires an inter frame with reference/mode-delta processing enabled, nonzero chroma filter levels,
intra, inter, and skipped-inter blocks; compares both decoded native frames exactly; compares final
presentation through ImageSharp's established reference-output API; and passes constrained tracked
allocation with balanced returns.
- [x] Removed an obsolete SVT-AV1 design link from mode-map documentation. Current official libaom
remains the sole external codec implementation source.
- [x] The focused Release checkpoint set passes 6/6 on net10.0 and 6/6 on net11.0, with zero failures
or skips.
- [x] Scoped analyzer verification passes for all four changed C# files. Roslynk reports zero compiler
errors, `git diff --check` passes, and `.gitattributes` is unchanged.
- [x] The completed checkpoint was committed as `fcb502e4960cc7b8efb06b6f060e2c73a913a2bf`
with author and committer `James Jackson-South <james_south@hotmail.com>`.
For every item:
- [ ] Trace syntax and arithmetic to the current libaom `main` tree.
- [ ] Execute the real production decoder path.
- [ ] Compare native planes exactly.
- [ ] Compare presentation through the established reference-image API.
- [ ] Run constrained allocator and exactly-once ownership coverage.
- [ ] Run FeatureTestRunner for SIMD and scalar dispatch when the implementation has SIMD.
- [ ] Record focused Release evidence before marking the item verified.
### 4. Close AV1 decoder coverage
Previously verified algorithm checkpoints remain valuable evidence, but the final decoder gate requires a fresh current-tree run after the inter and cleanup corrections.
- [x] Bounded OBU framing, sequence headers, frame headers, tile groups, alignment, and trailing-bit parsing have been re-audited and verified against current libaom `main`.
- [x] Partition traversal, mode information, segmentation, delta quantization, transform-size selection, coefficient decoding, inverse quantization, and inverse transforms have been re-audited and verified against current libaom `main`.
- [x] Intra prediction covers directional, DC, smooth, Paeth, chroma-from-luma, filter-intra, and palette families with the established operator architecture.
- [x] Intra-block copy has exact native reconstruction and feature-isolated SIMD evidence.
- [x] Lossless inverse transform, loop filtering, CDEF, super-resolution, restoration, and film grain have focused checkpoint evidence.
- [x] Retained references, CDF snapshots, segmentation maps, global motion, temporal motion fields, and dependent-frame lifecycle have been re-audited and verified against current libaom `main`.
- [x] The 12-case all-intra profile matrix covers every valid 8, 10, and 12-bit monochrome, 4:2:0, 4:2:2, and 4:4:4 combination. Dependent-frame coverage is recorded separately above.
- [x] The exact current-tree native-plane matrix passes through the production decoder on net10.0 and net11.0. The normal-dispatch and FeatureTestRunner fallback methods pass 2 of 2 focused tests on each target.
- [x] The exact current-tree presentation matrix passes 12 of 12 cases through ImageSharp's established reference-image API on net10.0 and net11.0.
- [x] Verify malformed/truncated data, frame IDs, reference slots, tile bounds, allocation limits, cancellation, and failure unwinding.
- [x] Verify still items and bounded sequences from file, memory, non-seekable, and short-read streams.
- [x] Verify ICC, CICP, alpha, grids, pixel aspect ratio, clean aperture, rotation, mirroring, metadata, and every presented sequence frame.
- [x] Complete the public AVIF format/API review so registered capabilities match implemented behavior.
- [x] Remove or reject every valid in-scope AV1 syntax branch that remains silently ignored or unsupported.
Verified negative-path and frame-identifier gate evidence on 2026-08-31:
- [x] A two-frame lossless frame-identifier sequence was generated and decoded with the clean official
libaom `main` checkout at observed revision `441c439b9916474cac15d2822af47a9ad70674a8`.
Both decoded frames match the source Y, Cb, and Cr samples exactly.
- [x] `DecodeFrameIdentifiersMatchReference` executes the production decoder through FeatureTestRunner,
compares both native frames exactly, and proves the second frame is dependent with a changed current
frame identifier. The current-frame, reference-delta, stale-slot, and refreshed-slot identifier logic
was audited against the same current `main` source.
- [x] The focused negative-path set passes 46 of 46 cases on net10.0 and 46 of 46 on net11.0, with zero
failures or skips. It covers truncated palette entropy, malformed-following-OBU recovery, parser
lifecycle failure, overflowing and invalid tile bounds, reference-slot ownership and transfer,
constrained multi-group allocation, motion-field allocation failure unwinding, and frame identifiers.
- [x] The established paused-stream cancellation suite now includes AVIF. It verifies cancellation at
0%, 30%, and 70% of both file and memory streams, plus pre-cancelled identification, on both targets.
- [x] The completed checkpoint was committed as
`7f0e08126b3354e8f1eb45886f0d572006ae27de` with author and committer
`James Jackson-South <james_south@hotmail.com>`.
Verified bounded-OBU checkpoint evidence on 2026-08-31:
- [x] Audited `av1/decoder/obu.c`, `av1/decoder/decodeframe.c`, `av1/common/obu_util.c`,
`av1/common/tile_common.c`, `aom/src/aom_integer.c`, and `aom_dsp/bitreader_buffer.c` in the
clean official libaom `main` checkout. Both `HEAD` and `origin/main` resolved to the observed
revision `441c439b9916474cac15d2822af47a9ad70674a8`; this is verification evidence, not a pin.
- [x] The bounded container scanner and production OBU reader now agree with current libaom on ignored
reserved header fields and the shared unsigned 32-bit LEB128 limit.
- [x] Sequence-header validation now rejects undefined level indices, initial display delays above ten,
frame identifiers above sixteen bits, zero timing units, the UVLC overflow sentinel, and invalid
identity-matrix profile or subsampling combinations at the owning syntax boundary.
- [x] Frame and tile parsing now rejects `show_existing_frame` in a combined `OBU_FRAME`, the all-slots
intra-only refresh mask, inner tile columns below current libaom's super-resolution-aware minimum,
overflowing or out-of-bounds tile sizes, and empty final tile payloads.
- [x] The still-image writer now emits the required zero tile-bound-presence bit for a multi-tile combined
`OBU_FRAME`, matching current libaom's single-tile-group encoder path.
- [x] `ObuFrameHeaderTests` and `ObuFrameLifecycleTests` cover the corrected syntax through the real
bounded parser. The focused parser set passes 50 of 50 cases on net10.0.
- [x] The final focused production set passes 55 of 55 cases on net10.0 and 55 of 55 on net11.0, with zero
failures or skips. It includes exact final-layer and selected-layer native planes, exact established
reference-image presentation, constrained allocator ownership, malformed-following-OBU recovery, and
FeatureTestRunner normal, AVX-512-disabled, AVX-disabled, and scalar execution.
- [x] A fresh direct foreground current-main `aomdec` run decoded both progressive layers with one thread
and row multithreading disabled. All 2,178 Y, U, and V samples match the retained YUV444-alpha reference;
the alpha plane is excluded from the AV1 native-plane comparison.
- [x] The current-libaom production reference test and its established PNG were renamed together. The PNG
bytes remain unchanged at SHA-256
`0758C17DC36E38AEE9F4389A335C2BF332AB91E4C79D7B0B22994FDDD0FD1605`, both paths resolve to
`diff=lfs`, and `.gitattributes` was not edited.
- [x] Release source builds pass for net10.0 and net11.0 with zero warnings and zero errors. Roslynk reports
zero compiler errors, and scoped production and test analyzer verification reports no changes.
- [x] The completed checkpoint was committed as `243524c2c0b52a49d8d161fab806ab092cabe47c` with author
and committer `James Jackson-South <james_south@hotmail.com>`.
Verified partition, mode, segmentation, quantization, and transform checkpoint evidence on 2026-08-31:
- [x] Audited partition traversal and chroma representability against `read_partition` and the subsampled
plane-size rejection in current libaom `av1/decoder/decodeframe.c`; spatial segment-ID decoding and
corruption handling against `read_segment_id` in `av1/decoder/decodemv.c`; delta-Q syntax, resolution,
arithmetic, and clamping against `read_delta_qindex` and `read_delta_q_params` in the same file.
- [x] Audited selected and variable transform-size traversal against `read_tx_size`, `read_tx_size_vartx`,
and transform-block traversal in `av1/decoder/decodeframe.c`; coefficient syntax and arithmetic against
`av1_read_coeffs_txb` in `av1/decoder/decodetxb.c`; inverse quantization and transform application against
current `av1/decoder/decodeframe.c`, `av1/common/idct.c`, and the current libaom transform test oracle.
The observed clean `HEAD` and `origin/main` revision was
`441c439b9916474cac15d2822af47a9ad70674a8`; this is verification evidence, not a pin.
- [x] Partition decoding now rejects an invalid partition subsize and a block size that cannot represent the
current subsampled chroma plane. Spatial segmentation rejects decoded IDs above the active segment range.
Focused tests exercise both current-libaom corruption boundaries through the production tile reader.
- [x] Coefficient entropy decoding uses one allocator-owned maximum-size `Av1LevelBuffer` per tile reader.
Each transform resets and clears only its active padded geometry, so no transform creates an allocation.
Allocation tracking over all eight minimum- and maximum-quantizer frames proves exactly one coefficient
scratch allocation per frame and exactly-once return after decoder disposal.
- [x] Palette index maps use two allocator-backed 128x128 decoder-session scratch buffers, one for luma and
one for chroma. Each parsed superblock is reconstructed before either buffer is reused, and each block
clears only its transient `Buffer2DRegion` view after prediction. The fixed 32 KiB session cost replaces
the former two full-frame maps without copies or per-block allocations. A 1 KiB constrained allocator
splits both buffers across memory groups. The four-case palette set passes on net10.0 and net11.0 with
exact native and presentation output, truncated-entropy rejection, and balanced exactly-once disposal.
- [x] `Av1BlockModeInfo` is value storage, removing the managed object allocation formerly created for every
decoded coding block. Explicit `ModeInfoIndex` values preserve libaom's mode-info identity semantics at
prediction-unit loop-filter edges, and the frame map now uses integer offsets so more than 65,535 decoded
blocks cannot wrap its lookup identity.
- [x] Current official libaom reproduced the 39-frame all-intra reference and all four 8/10-bit minimum- and
maximum-quantizer references byte for byte. The production tests compare every native sample exactly,
cover every intra mode and seven selected transform types, execute SIMD and scalar paths through
`FeatureTestRunner`, and exercise the quantizer sequences under constrained tracked allocation.
- [x] Current official libaom decoded the 42-byte palette payload into the retained 1,089-byte YUV444
reference at SHA-256 `E05F7C0DF06ECCF0E43869D1D7B03DAA1D635ACD26A766F8940899BE18D53251`.
The exact native test requires luma and chroma palette syntax. The established reference-output test uses
the unchanged presentation PNG at SHA-256
`1148EBF6AA4B0F2D069D5E9B9605F6FB2A315E525F18016CDCAE23EFDD81DA84`, whose renamed path still
resolves to `diff=lfs`; `.gitattributes` was not edited.
- [x] The exact final AV1 namespace passes 8,732 of 8,732 cases on net10.0 and 8,732 of 8,732 cases on
net11.0, with zero failures or skips. Release source builds pass for net10.0 and net11.0 with zero warnings
and zero errors. Roslynk reports zero compiler errors, and scoped analyzer verification reports no changes.
- [x] The completed checkpoint was committed as `57a3f6668e39d0934e7b6b8d37a3dc2a5adc88f0` with author
and committer `James Jackson-South <james_south@hotmail.com>`.
Verified retained-frame lifecycle checkpoint evidence on 2026-08-31:
- [x] Audited primary-reference entropy selection, independent per-tile CDF starts, context-update-tile
publication, segmentation-map inheritance, reference-map refresh, and show-existing key-frame reset
against current libaom `av1/decoder/decodeframe.c`, `av1/decoder/decodemv.c`,
`av1/decoder/decoder.c`, and `av1/common/entropymode.c`.
- [x] Audited retained motion-vector cells, reference-side classification, projection source ordering,
projection limits, and reference-frame publication against `av1_copy_frame_mvs`,
`av1_calculate_ref_frame_side`, `motion_field_projection`, and `av1_setup_motion_field` in current
libaom. Same-role primary-reference global-motion inheritance remains covered by the exact current-main
global-warp fixture. The observed clean `HEAD` and `origin/main` revision was
`441c439b9916474cac15d2822af47a9ad70674a8`; this is verification evidence, not a pin.
- [x] Current official libaom decoded the retained `cdfupdate`, `mfmv`, `svc-L2T1`, `svc-L1T2`, and
`svc-L2T2` streams with one thread, row threading disabled, and eight-bit output depth. Their generated
Y4M files match the retained references byte for byte at SHA-256
`4FBFF73FF0DE2D9084DAE557D1D4BD677B0486516525BF4D327D2D795D5A7779`,
`F7DB607694818C19E62FD9A27F53E1A3E2D00B72C39C0430C1B26399CC76777D`,
`7A427631ECBF144F435AA4612F1201415FB1A9BCF9A67BA010AEF830B0C3AB81`,
`4012DE2D4AFD095E7BB68EAE18B50B0674781BB4971CECABC0E5471E63373ED3`, and
`1ABB981CFF76BA9557DA437B258D8A95FCA755DED8E3949D857E8388AB1D6AE3`.
- [x] Existing allocation-tracking tests exercise initialization, retained-slot aliases, allocation-failure
unwinding, presentation ownership, decoder-result ownership, repeated disposal, and final exactly-once
return of reference frames, frame-owned motion fields, entropy snapshots, and segmentation maps.
- [x] The focused Release checkpoint set passes 54 of 54 cases on net10.0 and 54 of 54 cases on net11.0,
with zero failures or skips. It includes exact native CDF-update, motion-field, spatial-layer,
temporal-layer, spatial-temporal-layer, progressive dependent-frame, and global-warp production paths,
plus constrained allocator coverage.
- [x] Release source builds pass for net10.0 and net11.0 with zero warnings and zero errors. Roslynk
reports zero compiler errors, scoped analyzer verification reports no changes, `git diff --check`
passes, and `.gitattributes` is unchanged.
Final decoder allocation, lifetime, precision, architecture, and test-validity audit evidence on 2026-09-01:
- [x] Refreshed the official libaom remote and audited against observed `origin/main`
`976867526367f571a1c09b994066af8364aed781`. The intervening external-rate-controller commit does
not change `av1/decoder`, `av1/common`, `aom_dsp`, or the AV1 decoder build definition.
- [x] CDEF now uses one bounded 64x64-unit bordered source workspace, two preserved top-row slots per
plane, preserved left columns, and unit-local direction and variance storage. This replaces the
frame-wide source copy and frame-wide direction maps while retaining libaom's unit traversal and
cross-plane luma-direction lifetime.
- [x] Loop restoration now retains the required immutable source and separate destination, but stores the
full destination in native sample width. Eight-bit filtering narrows only bounded unit output after
clipping, while high-bit-depth filtering writes directly to the native `ushort` destination.
- [x] Reference-to-presentation copying now copies visible native rows only. Padding remains destination
owned, and the ownership tests mutate a copied visible sample rather than unrelated padding.
- [x] The remaining decoder allocations and copies are either bounded scratch or required ownership
boundaries. Frame planes enforce their contiguous single-span invariant before allocation; palette,
transform, film-grain, super-resolution, color-conversion, and alpha workspaces remain bounded and
allocator owned. No per-block managed allocation remains in reconstruction.
- [x] Valid unsupported tile-list syntax is rejected explicitly. Reserved and metadata OBUs are consumed
only after bounded framing and trailing-bit validation. Eight-, ten-, and twelve-bit reconstruction,
presentation, alpha, restoration, and film-grain paths retain native precision.
- [x] Predictor traversal remains split into semantic readonly operator families. The planar sample
adapter and transform-block context are value types, and Release construction sites use `default`
without null-forgiving suppression.
- [x] The net11.0 Release test project builds with zero errors. Roslynk reports zero compiler errors,
`git diff --check` passes, and `.gitattributes` is unchanged.
- [x] Visual Studio 18.9 VSTest ran the complete `Formats.Heif.Av1` namespace with collection
parallelism disabled and stop-on-failure enabled: 8,746 of 8,746 cases passed. The touched
`HeifDecoderTests` and `HeifSequenceParserTests` add 104 of 104 passing integration cases.
Focused CDEF, restoration, film-grain, copy-ownership, and reference-isolation runs also pass 15 of
15 cases. No test-host crash or Windows application-error dialog occurred.
Final decoder stream, presentation, and public-registration evidence on 2026-09-01:
- [x] Real AV1 still-item and timed-sequence files decode identically from a file stream, memory stream,
non-seekable stream, and a seekable stream limited to three bytes per read. All eight stream rows pass
through public format detection and production decoding, comparing every presented frame exactly.
- [x] A two-frame production sequence applies a centered clean-aperture crop, counter-clockwise rotation,
mirroring, pixel-aspect-ratio metadata, and CICP metadata to every frame. The complete five-frame real
auxiliary-alpha sequence composes non-opaque alpha and retains timing, Exif, and XMP for every frame.
- [x] The fixed-header detector accepts both compact and extended-size leading file-type boxes. Default
configuration registers the implemented HEIF decoder and detector but no longer advertises the
incomplete HEIF encoder.
- [x] Visual Studio 18.9 VSTest, serialized with stop-on-failure enabled, passes the 12 of 12 new
stream/presentation/registration cases and the complete current `HeifDecoderTests` plus
`HeifSequenceParserTests` set with the registration contract: 115 of 115. The final explicit
no-encoder registration assertion passes 1 of 1 after its final edit.
- [x] The net11.0 Release test project builds with zero errors, Roslynk reports zero compiler errors,
`git diff --check` passes, and `.gitattributes` is unchanged. Every VSTest invocation returned
normally with no surviving test host and no Windows application-error dialog.
Decoder exit gate:
- [x] Every supported native format and AV1 tool has exact current-main libaom production-path evidence.
- [x] Every supported presentation behavior has established reference-image evidence at the correct output precision.
- [x] No decoder path relies on a native codec, copied plane, per-block allocation, or contiguous memory-group accident.
- [x] All allocator ownership is deterministic and exactly once.
- [x] Full focused Release verification is recorded with no false coverage claims.
## AV1 encoder implementation
Writer primitives are not an encoder. The public encoder remains incomplete until it produces independently decodable AV1 payloads and AVIF containers for every exposed option.
### 5. Define and enforce the encoder contract
- [ ] Finalize observable options for quality, effort, lossless mode, bit depth, chroma subsampling, alpha quality, metadata, and bounded sequences.
- [ ] Preserve high-bit-depth source precision through 16-bit RGB and native 10/12-bit component planes.
- [ ] Reject unsupported combinations at the public boundary before writing output.
- [ ] Register only capabilities that the completed encoder proves.
### 6. Build the complete AV1 frame encoder
- [~] SIMD-first RGB-to-native-plane conversion exists locally.
- [~] Forward transform families and transform workspace exist locally.
- [~] Symbol writer, coefficient writer, and tile writer fragments exist locally.
- [ ] Connect a frame-owned encoder lifecycle using ImageSharp allocators and pools.
- [ ] Write compliant temporal delimiter, sequence header, frame header, tile group, metadata, and padding OBUs as required.
- [ ] Implement superblock and partition analysis for every permitted block size and partition.
- [ ] Implement intra mode search, chroma mode search, palette, filter intra, chroma-from-luma, and intra-block copy decisions.
- [ ] Implement inter mode search for bounded sequences, including reference selection and the decoder-supported inter tools.
- [ ] Implement transform-size/type search, forward transform, quantization, coefficient optimization, and lossless behavior.
- [ ] Implement real rate-distortion selection and make quality and effort change work, size, and output quality.
- [ ] Implement tile-local entropy coding and CDF update behavior.
- [ ] Implement legal deblocking, CDEF, restoration, super-resolution, and film-grain signaling decisions.
- [ ] Remove per-transform and per-block managed allocations from active encoder paths.
- [ ] Use descending SIMD dispatch: Vector512, Vector256, Vector128, then scalar.
- [ ] Verify every SIMD operator with FeatureTestRunner and an independent scalar oracle shaped from the same current-main libaom behavior.
### 7. Write complete AVIF output
- [ ] Write the correct AVIF file type, item information, locations, references, properties, AV1 configuration, dimensions, color, alpha, metadata, and media data.
- [ ] Support single images, alpha auxiliary images, grids, multiple extents, and bounded image sequences in the final public scope.
- [ ] Preserve ICC, Exif, and XMP according to encoder options.
- [ ] Write CICP, range, chroma position, bit depth, and subsampling values that match the encoded planes.
- [ ] Apply orientation and clean-aperture behavior consistently with ImageSharp encoder conventions.
- [ ] Stream output through allocator-backed chunked storage without file-sized copies or ToArray materialization.
Encoder exit gate:
- [ ] Current-main libaom accepts every produced AV1 payload.
- [ ] Lossless output is exact at native-plane and final-pixel precision.
- [ ] Lossy output demonstrates recorded quality and effort tradeoffs with absolute size, quality, timing, and allocation evidence.
- [ ] 8, 10, and 12-bit monochrome, 4:2:0, 4:2:2, and 4:4:4 outputs pass.
- [ ] Alpha, grids, metadata, color profiles, transforms, and bounded sequences pass.
- [ ] ImageSharp decode of its own output is supplemental coverage only, never the sole oracle.
- [ ] Public encoding no longer throws for a supported AV1 request.
- [ ] Focused Release and FeatureTestRunner verification passes with exact recorded evidence.
## Architecture rules
- Follow the JPEG color-converter operator architecture exactly.
- Each distinct prediction traversal owns a family-named predictor type.
- The family .Operator.cs file defines the nested static operator contract.
- Each semantic readonly struct belongs to that owner and implements concrete scalar, Vector128, Vector256, and Vector512 arithmetic for the shared traversal.
- Do not place a distinct predictor beneath a broad Av1IntraPredictor or Av1InterPredictor.
- Do not create semantic forwarding wrappers, top-level operator types, hardware-width-named operator types, CRTP contracts, or one file containing unrelated semantic operators.
- Forward transforms belong to Av1ForwardTransformer and its semantic operator files.
- Inverse axis transforms belong to Av1Inverse2dTransformer and its semantic operator files.
- Reconstruction output operators belong to Av1InverseTransformer.
- Shared lane primitives belong only in explicitly named Operations types.
- Dispatch from widest to narrowest supported SIMD width, then execute one scalar tail.
- Keep codec execution sequential. Do not add parallel execution inside the codec.
- Do not allocate per row, block, transform, scanline, or SIMD tail.
- Use ImageSharp allocators and pools. Do not use ToArray to cross an ownership boundary.
- On internal types, use public members when other types consume them; reserve private members for type-local behavior.
- Use established ImageSharp test data, allocator tracking, FeatureTestRunner, and reference-image comparison APIs. Do not build custom substitutes.
- Public XML documentation describes observable behavior only.
- Inline comments explain the current-libaom numerical rule, ownership boundary, edge extension, entropy ordering, or SIMD shape at technically complex points.
- Every multiline statement or declaration is followed by vertical whitespace.
- Do not edit .gitattributes directly.
- Do not install or download tools without explicit permission.
## Final verification matrix
- [ ] Release source build: net10.0, zero errors.
- [ ] Release source build: net11.0, zero errors.
- [ ] Scoped semantic inspection: zero compiler errors attributable to this work.
- [ ] Focused decoder syntax, reconstruction, ownership, presentation, and malformed-input tests.
- [ ] Focused encoder syntax, payload, container, precision, ownership, and option tests.
- [ ] FeatureTestRunner coverage for normal, narrower SIMD tiers, and scalar fallback.
- [ ] Constrained multi-group allocator coverage with balanced exactly-once returns.
- [ ] Exact native-plane comparisons against current-main libaom.
- [ ] Established final-presentation comparisons at the target pixel precision.
- [ ] Scoped StyleCop and vertical-whitespace inspection.
- [ ] No stale unsupported capability claims or removed-code references.
- [ ] No restore-source failures, background test hosts, detached processes, or crash-report popups.
- [ ] .gitattributes unchanged.
- [ ] git diff --check clean.
- [ ] Documentation records exact commands, counts, current-main reference revision evidence, and results.
- [ ] Commit only after the relevant checkpoint is genuinely complete.
- [ ] Do not push.

5
ImageSharp.slnx

@ -114,6 +114,11 @@
<File Path="tests/Images/Input/Gif/issues/issue405_badappextlength252-2.gif" />
<File Path="tests/Images/Input/Gif/issues/issue405_badappextlength252.gif" />
</Folder>
<Folder Name="/tests/Images/Input/Heif/">
<File Path="tests/Images/Input/Heif/Irvine_CA.avif" />
<File Path="tests/Images/Input/Heif/jpeg444_xnconvert.avif" />
<File Path="tests/Images/Input/Heif/Orange4x4.avif" />
</Folder>
<Folder Name="/tests/Images/Input/Icon/">
<File Path="tests/Images/Input/Icon/aero_arrow.cur" />
<File Path="tests/Images/Input/Icon/flutter.ico" />

142
THIRD-PARTY-NOTICES.TXT

@ -30,3 +30,145 @@ See the LICENSE file in the CoreFX project root for more information.
https://github.com/dotnet/corefx/blob/17300169760c61a90cab8d913636c1058a30a8c1/LICENSE.TXT
https://github.com/dotnet/corefx/blob/17300169760c61a90cab8d913636c1058a30a8c1/src/Common/src/CoreLib/System/IO/Stream.cs#L742
https://github.com/dotnet/corefx/blob/17300169760c61a90cab8d913636c1058a30a8c1/src/Common/src/CoreLib/System/IO/Stream.cs#L775
License notice for the AOM AV1 Codec Library
-----
Parts of the AV1 implementation are adapted from the AOM AV1 Codec Library.
https://aomedia.googlesource.com/aom/
Copyright (c) 2016, Alliance for Open Media. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
Alliance for Open Media Patent License 1.0
1. License Terms.
1.1. Patent License. Subject to the terms and conditions of this License, each
Licensor, on behalf of itself and successors in interest and assigns,
grants Licensee a non-sublicensable, perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as expressly stated in this
License) patent license to its Necessary Claims to make, use, sell, offer
for sale, import or distribute any Implementation.
1.2. Conditions.
1.2.1. Availability. As a condition to the grant of rights to Licensee to make,
sell, offer for sale, import or distribute an Implementation under
Section 1.1, Licensee must make its Necessary Claims available under
this License, and must reproduce this License with any Implementation
as follows:
a. For distribution in source code, by including this License in the
root directory of the source code with its Implementation.
b. For distribution in any other form (including binary, object form,
and/or hardware description code (e.g., HDL, RTL, Gate Level Netlist,
GDSII, etc.)), by including this License in the documentation, legal
notices, and/or other written materials provided with the
Implementation.
1.2.2. Additional Conditions. This license is directly from Licensor to
Licensee. Licensee acknowledges as a condition of benefiting from it
that no rights from Licensor are received from suppliers, distributors,
or otherwise in connection with this License.
1.3. Defensive Termination. If any Licensee, its Affiliates, or its agents
initiates patent litigation or files, maintains, or voluntarily
participates in a lawsuit against another entity or any person asserting
that any Implementation infringes Necessary Claims, any patent licenses
granted under this License directly to the Licensee are immediately
terminated as of the date of the initiation of action unless 1) that suit
was in response to a corresponding suit regarding an Implementation first
brought against an initiating entity, or 2) that suit was brought to
enforce the terms of this License (including intervention in a third-party
action by a Licensee).
1.4. Disclaimers. The Reference Implementation and Specification are provided
"AS IS" and without warranty. The entire risk as to implementing or
otherwise using the Reference Implementation or Specification is assumed
by the implementer and user. Licensor expressly disclaims any warranties
(express, implied, or otherwise), including implied warranties of
merchantability, non-infringement, fitness for a particular purpose, or
title, related to the material. IN NO EVENT WILL LICENSOR BE LIABLE TO
ANY OTHER PARTY FOR LOST PROFITS OR ANY FORM OF INDIRECT, SPECIAL,
INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER FROM ANY CAUSES OF
ACTION OF ANY KIND WITH RESPECT TO THIS LICENSE, WHETHER BASED ON BREACH
OF CONTRACT, TORT (INCLUDING NEGLIGENCE), OR OTHERWISE, AND WHETHER OR
NOT THE OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2. Definitions.
2.1. Affiliate. "Affiliate" means an entity that directly or indirectly
Controls, is Controlled by, or is under common Control of that party.
2.2. Control. "Control" means direct or indirect control of more than 50% of
the voting power to elect directors of that corporation, or for any other
entity, the power to direct management of such entity.
2.3. Decoder. "Decoder" means any decoder that conforms fully with all
non-optional portions of the Specification.
2.4. Encoder. "Encoder" means any encoder that produces a bitstream that can
be decoded by a Decoder only to the extent it produces such a bitstream.
2.5. Final Deliverable. "Final Deliverable" means the final version of a
deliverable approved by the Alliance for Open Media as a Final
Deliverable.
2.6. Implementation. "Implementation" means any implementation, including the
Reference Implementation, that is an Encoder and/or a Decoder. An
Implementation also includes components of an Implementation only to the
extent they are used as part of an Implementation.
2.7. License. "License" means this license.
2.8. Licensee. "Licensee" means any person or entity who exercises patent
rights granted under this License.
2.9. Licensor. "Licensor" means (i) any Licensee that makes, sells, offers
for sale, imports or distributes any Implementation, or (ii) a person
or entity that has a licensing obligation to the Implementation as a
result of its membership and/or participation in the Alliance for Open
Media working group that developed the Specification.
2.10. Necessary Claims. "Necessary Claims" means all claims of patents or
patent applications, (a) that currently or at any time in the future,
are owned or controlled by the Licensor, and (b) (i) would be an
Essential Claim as defined by the W3C Policy as of February 5, 2004
(https://www.w3.org/Consortium/Patent-Policy-20040205/#def-essential)
as if the Specification was a W3C Recommendation; or (ii) are infringed
by the Reference Implementation.
2.11. Reference Implementation. "Reference Implementation" means an Encoder
and/or Decoder released by the Alliance for Open Media as a Final
Deliverable.
2.12. Specification. "Specification" means the specification designated by
the Alliance for Open Media as a Final Deliverable for which this
License was issued.

2
shared-infrastructure

@ -1 +1 @@
Subproject commit 52fc1c3e87a755b2b17ce77e8cd22ec4bea17bcb
Subproject commit a835a9d74e82b2d32b580a7902eb2699ebc47098

53
src/ImageSharp/Common/Helpers/DisposableDictionary.cs

@ -0,0 +1,53 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Common.Helpers;
/// <summary>
/// Dictionary of <see cref="IDisposable"/> objects, which is itself <see cref="IDisposable"/>.
/// </summary>
/// <typeparam name="TKey">The type of the key.</typeparam>
/// <typeparam name="TValue">Tye type of value, needs to implement <see cref="IDisposable"/>.</typeparam>
public sealed class DisposableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IDisposable
where TKey : notnull
where TValue : IDisposable
{
private bool disposedValue;
/// <inheritdoc />
public DisposableDictionary()
: base()
{
}
/// <inheritdoc />
public DisposableDictionary(int capacity)
: base(capacity)
{
}
/// <inheritdoc />
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
this.Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!this.disposedValue)
{
if (disposing)
{
foreach (KeyValuePair<TKey, TValue> pair in this)
{
pair.Value?.Dispose();
}
}
this.Clear();
this.disposedValue = true;
}
}
}

51
src/ImageSharp/Common/Helpers/DisposableList.cs

@ -0,0 +1,51 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Common.Helpers;
/// <summary>
/// List of <see cref="IDisposable"/> objects, which is itself <see cref="IDisposable"/>.
/// </summary>
/// <typeparam name="TValue">Tye type of value, needs to implement <see cref="IDisposable"/>.</typeparam>
public sealed class DisposableList<TValue> : List<TValue>, IDisposable
where TValue : IDisposable
{
private bool disposedValue;
/// <inheritdoc />
public DisposableList()
: base()
{
}
/// <inheritdoc />
public DisposableList(int capacity)
: base(capacity)
{
}
/// <inheritdoc />
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
this.Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!this.disposedValue)
{
if (disposing)
{
foreach (TValue item in this)
{
item?.Dispose();
}
}
this.Clear();
this.disposedValue = true;
}
}
}

185
src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs

@ -9,13 +9,12 @@ using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp;
internal static partial class SimdUtils
{
public static class HwIntrinsics
public static partial class HwIntrinsics
{
#pragma warning disable SA1117 // Parameters should be on same line or separate lines
#pragma warning disable SA1137 // Elements should have the same indentation
@ -25,31 +24,15 @@ internal static partial class SimdUtils
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<int> PermuteMaskDeinterleave16x32() => Vector512.Create(0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<uint> PermuteMaskEvenOdd8x32() => Vector256.Create(0, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 7, 0, 0, 0).AsUInt32();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<uint> PermuteMaskSwitchInnerDWords8x32() => Vector256.Create(0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0).AsUInt32();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<uint> MoveFirst24BytesToSeparateLanes() => Vector256.Create(0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 6, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 7, 0, 0, 0).AsUInt32();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector256<byte> ExtractRgb() => Vector256.Create(0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11, 0xFF, 0xFF, 0xFF, 0xFF, 0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11, 0xFF, 0xFF, 0xFF, 0xFF);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> ShuffleMaskPad4Nx16() => Vector128.Create(0, 1, 2, 0x80, 3, 4, 5, 0x80, 6, 7, 8, 0x80, 9, 10, 11, 0x80);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> ShuffleMaskSlice4Nx16() => Vector128.Create(0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 0x80, 0x80, 0x80, 0x80);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<byte> ShuffleMaskShiftAlpha() => Vector256.Create(
(byte)0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 3, 7, 11, 15,
0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 3, 7, 11, 15);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<uint> PermuteMaskShiftAlpha8x32() => Vector256.Create(0u, 1, 2, 4, 5, 6, 3, 7);
#pragma warning restore SA1137 // Elements should have the same indentation
#pragma warning restore SA1117 // Parameters should be on same line or separate lines
@ -1038,171 +1021,5 @@ internal static partial class SimdUtils
}
}
}
internal static void PackFromRgbPlanesAvx2Reduce(
ref ReadOnlySpan<byte> redChannel,
ref ReadOnlySpan<byte> greenChannel,
ref ReadOnlySpan<byte> blueChannel,
ref Span<Rgb24> destination)
{
ref Vector256<byte> rBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(redChannel));
ref Vector256<byte> gBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(greenChannel));
ref Vector256<byte> bBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(blueChannel));
ref byte dBase = ref Unsafe.As<Rgb24, byte>(ref MemoryMarshal.GetReference(destination));
nuint count = redChannel.Vector256Count<byte>();
Vector256<uint> control1 = PermuteMaskEvenOdd8x32();
Vector256<uint> control2 = PermuteMaskShiftAlpha8x32();
Vector256<byte> a = Vector256.Create((byte)255);
Vector256<byte> shuffleAlpha = ShuffleMaskShiftAlpha();
for (nuint i = 0; i < count; i++)
{
Vector256<byte> r0 = Unsafe.Add(ref rBase, i);
Vector256<byte> g0 = Unsafe.Add(ref gBase, i);
Vector256<byte> b0 = Unsafe.Add(ref bBase, i);
r0 = Avx2.PermuteVar8x32(r0.AsUInt32(), control1).AsByte();
g0 = Avx2.PermuteVar8x32(g0.AsUInt32(), control1).AsByte();
b0 = Avx2.PermuteVar8x32(b0.AsUInt32(), control1).AsByte();
Vector256<byte> rg = Avx2.UnpackLow(r0, g0);
Vector256<byte> b1 = Avx2.UnpackLow(b0, a);
Vector256<byte> rgb1 = Avx2.UnpackLow(rg.AsUInt16(), b1.AsUInt16()).AsByte();
Vector256<byte> rgb2 = Avx2.UnpackHigh(rg.AsUInt16(), b1.AsUInt16()).AsByte();
rg = Avx2.UnpackHigh(r0, g0);
b1 = Avx2.UnpackHigh(b0, a);
Vector256<byte> rgb3 = Avx2.UnpackLow(rg.AsUInt16(), b1.AsUInt16()).AsByte();
Vector256<byte> rgb4 = Avx2.UnpackHigh(rg.AsUInt16(), b1.AsUInt16()).AsByte();
rgb1 = Avx2.Shuffle(rgb1, shuffleAlpha);
rgb2 = Avx2.Shuffle(rgb2, shuffleAlpha);
rgb3 = Avx2.Shuffle(rgb3, shuffleAlpha);
rgb4 = Avx2.Shuffle(rgb4, shuffleAlpha);
rgb1 = Avx2.PermuteVar8x32(rgb1.AsUInt32(), control2).AsByte();
rgb2 = Avx2.PermuteVar8x32(rgb2.AsUInt32(), control2).AsByte();
rgb3 = Avx2.PermuteVar8x32(rgb3.AsUInt32(), control2).AsByte();
rgb4 = Avx2.PermuteVar8x32(rgb4.AsUInt32(), control2).AsByte();
ref byte d1 = ref Unsafe.Add(ref dBase, 24 * 4 * i);
ref byte d2 = ref Unsafe.Add(ref d1, 24);
ref byte d3 = ref Unsafe.Add(ref d2, 24);
ref byte d4 = ref Unsafe.Add(ref d3, 24);
Unsafe.As<byte, Vector256<byte>>(ref d1) = rgb1;
Unsafe.As<byte, Vector256<byte>>(ref d2) = rgb2;
Unsafe.As<byte, Vector256<byte>>(ref d3) = rgb3;
Unsafe.As<byte, Vector256<byte>>(ref d4) = rgb4;
}
int slice = (int)count * Vector256<byte>.Count;
redChannel = redChannel[slice..];
greenChannel = greenChannel[slice..];
blueChannel = blueChannel[slice..];
destination = destination[slice..];
}
internal static void PackFromRgbPlanesAvx2Reduce(
ref ReadOnlySpan<byte> redChannel,
ref ReadOnlySpan<byte> greenChannel,
ref ReadOnlySpan<byte> blueChannel,
ref Span<Rgba32> destination)
{
ref Vector256<byte> rBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(redChannel));
ref Vector256<byte> gBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(greenChannel));
ref Vector256<byte> bBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(blueChannel));
ref Vector256<byte> dBase = ref Unsafe.As<Rgba32, Vector256<byte>>(ref MemoryMarshal.GetReference(destination));
nuint count = redChannel.Vector256Count<byte>();
Vector256<uint> control1 = PermuteMaskEvenOdd8x32();
Vector256<byte> a = Vector256.Create((byte)255);
for (nuint i = 0; i < count; i++)
{
Vector256<byte> r0 = Unsafe.Add(ref rBase, i);
Vector256<byte> g0 = Unsafe.Add(ref gBase, i);
Vector256<byte> b0 = Unsafe.Add(ref bBase, i);
r0 = Avx2.PermuteVar8x32(r0.AsUInt32(), control1).AsByte();
g0 = Avx2.PermuteVar8x32(g0.AsUInt32(), control1).AsByte();
b0 = Avx2.PermuteVar8x32(b0.AsUInt32(), control1).AsByte();
Vector256<byte> rg = Avx2.UnpackLow(r0, g0);
Vector256<byte> b1 = Avx2.UnpackLow(b0, a);
Vector256<byte> rgb1 = Avx2.UnpackLow(rg.AsUInt16(), b1.AsUInt16()).AsByte();
Vector256<byte> rgb2 = Avx2.UnpackHigh(rg.AsUInt16(), b1.AsUInt16()).AsByte();
rg = Avx2.UnpackHigh(r0, g0);
b1 = Avx2.UnpackHigh(b0, a);
Vector256<byte> rgb3 = Avx2.UnpackLow(rg.AsUInt16(), b1.AsUInt16()).AsByte();
Vector256<byte> rgb4 = Avx2.UnpackHigh(rg.AsUInt16(), b1.AsUInt16()).AsByte();
ref Vector256<byte> d0 = ref Unsafe.Add(ref dBase, i * 4);
d0 = rgb1;
Unsafe.Add(ref d0, 1) = rgb2;
Unsafe.Add(ref d0, 2) = rgb3;
Unsafe.Add(ref d0, 3) = rgb4;
}
int slice = (int)count * Vector256<byte>.Count;
redChannel = redChannel[slice..];
greenChannel = greenChannel[slice..];
blueChannel = blueChannel[slice..];
destination = destination[slice..];
}
internal static void UnpackToRgbPlanesAvx2Reduce(
ref Span<float> redChannel,
ref Span<float> greenChannel,
ref Span<float> blueChannel,
ref ReadOnlySpan<Rgb24> source)
{
ref Vector256<byte> rgbByteSpan = ref Unsafe.As<Rgb24, Vector256<byte>>(ref MemoryMarshal.GetReference(source));
ref Vector256<float> destRRef = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(redChannel));
ref Vector256<float> destGRef = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(greenChannel));
ref Vector256<float> destBRef = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(blueChannel));
Vector256<uint> extractToLanesMask = MoveFirst24BytesToSeparateLanes();
Vector256<byte> extractRgbMask = ExtractRgb();
Vector256<byte> rgb, rg, bx;
Vector256<float> r, g, b;
// Each iteration consumes 8 Rgb24 pixels (24 bytes) but starts with a 32-byte load,
// so we need 3 extra pixels of addressable slack beyond the vectorized chunk.
const int bytesPerRgbStride = 24;
nuint count = source.Length > 3 ? (uint)(source.Length - 3) / 8 : 0;
for (nuint i = 0; i < count; i++)
{
rgb = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref rgbByteSpan, (uint)(bytesPerRgbStride * i)).AsUInt32(), extractToLanesMask).AsByte();
rgb = Avx2.Shuffle(rgb, extractRgbMask);
rg = Avx2.UnpackLow(rgb, Vector256<byte>.Zero);
bx = Avx2.UnpackHigh(rgb, Vector256<byte>.Zero);
r = Avx.ConvertToVector256Single(Avx2.UnpackLow(rg, Vector256<byte>.Zero).AsInt32());
g = Avx.ConvertToVector256Single(Avx2.UnpackHigh(rg, Vector256<byte>.Zero).AsInt32());
b = Avx.ConvertToVector256Single(Avx2.UnpackLow(bx, Vector256<byte>.Zero).AsInt32());
Unsafe.Add(ref destRRef, i) = r;
Unsafe.Add(ref destGRef, i) = g;
Unsafe.Add(ref destBRef, i) = b;
}
int sliceCount = (int)(count * 8);
redChannel = redChannel[sliceCount..];
greenChannel = greenChannel[sliceCount..];
blueChannel = blueChannel[sliceCount..];
source = source[sliceCount..];
}
}
}

472
src/ImageSharp/Common/Helpers/SimdUtils.Pack.cs

@ -3,7 +3,9 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp;
@ -19,11 +21,16 @@ internal static partial class SimdUtils
{
DebugGuard.IsTrue(greenChannel.Length == redChannel.Length, nameof(greenChannel), "Channels must be of same size!");
DebugGuard.IsTrue(blueChannel.Length == redChannel.Length, nameof(blueChannel), "Channels must be of same size!");
DebugGuard.IsTrue(destination.Length > redChannel.Length + 2, nameof(destination), "'destination' must contain a padding of 3 elements!");
DebugGuard.IsTrue(destination.Length >= redChannel.Length, nameof(destination), "'destination' span should not be shorter than the source channels!");
if (Avx2.IsSupported)
{
HwIntrinsics.PackFromRgbPlanesAvx2Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination);
HwIntrinsics.PackFromRgbPlanesReduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination);
}
if (Vector128.IsHardwareAccelerated)
{
PackFromRgbPlanesVector128Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination);
}
else
{
@ -42,11 +49,16 @@ internal static partial class SimdUtils
{
DebugGuard.IsTrue(greenChannel.Length == redChannel.Length, nameof(greenChannel), "Channels must be of same size!");
DebugGuard.IsTrue(blueChannel.Length == redChannel.Length, nameof(blueChannel), "Channels must be of same size!");
DebugGuard.IsTrue(destination.Length > redChannel.Length, nameof(destination), "'destination' span should not be shorter than the source channels!");
DebugGuard.IsTrue(destination.Length >= redChannel.Length, nameof(destination), "'destination' span should not be shorter than the source channels!");
if (Avx2.IsSupported)
{
HwIntrinsics.PackFromRgbPlanesAvx2Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination);
HwIntrinsics.PackFromRgbPlanesReduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination);
}
if (Vector128.IsHardwareAccelerated)
{
PackFromRgbPlanesVector128Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination);
}
else
{
@ -69,12 +81,143 @@ internal static partial class SimdUtils
if (Avx2.IsSupported)
{
HwIntrinsics.UnpackToRgbPlanesAvx2Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref source);
HwIntrinsics.UnpackToRgbPlanesReduce(ref redChannel, ref greenChannel, ref blueChannel, ref source);
}
UnpackToRgbPlanesScalar(redChannel, greenChannel, blueChannel, source);
}
/// <summary>
/// Packs complete sixteen-pixel batches into exact-length <see cref="Rgb24"/> storage using portable 128-bit SIMD.
/// </summary>
/// <param name="redChannel">The red source span, advanced past the converted batches.</param>
/// <param name="greenChannel">The green source span, advanced past the converted batches.</param>
/// <param name="blueChannel">The blue source span, advanced past the converted batches.</param>
/// <param name="destination">The destination span, advanced past the converted batches.</param>
private static void PackFromRgbPlanesVector128Reduce(
ref ReadOnlySpan<byte> redChannel,
ref ReadOnlySpan<byte> greenChannel,
ref ReadOnlySpan<byte> blueChannel,
ref Span<Rgb24> destination)
{
ref byte redBase = ref MemoryMarshal.GetReference(redChannel);
ref byte greenBase = ref MemoryMarshal.GetReference(greenChannel);
ref byte blueBase = ref MemoryMarshal.GetReference(blueChannel);
ref byte destinationBase = ref Unsafe.As<Rgb24, byte>(ref MemoryMarshal.GetReference(destination));
Vector128<byte> opaqueAlpha = Vector128.Create(byte.MaxValue);
Vector128<byte> removeAlpha = Vector128.Create((byte)0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue);
nuint batchCount = (nuint)(uint)redChannel.Length / (uint)Vector128<byte>.Count;
for (nuint i = 0; i < batchCount; i++)
{
nuint sourceOffset = i * (uint)Vector128<byte>.Count;
Vector128<byte> red = Vector128.LoadUnsafe(ref redBase, sourceOffset);
Vector128<byte> green = Vector128.LoadUnsafe(ref greenBase, sourceOffset);
Vector128<byte> blue = Vector128.LoadUnsafe(ref blueBase, sourceOffset);
InterleaveRgbPlanes(red, green, blue, opaqueAlpha, out Vector128<byte> rgba0, out Vector128<byte> rgba1, out Vector128<byte> rgba2, out Vector128<byte> rgba3);
// The native byte shuffle removes alpha from four pixels at a time. Each result owns twelve bytes, so
// exact stores avoid coupling the SIMD path to padding beyond the row or the next memory-group segment.
ref byte destination0 = ref Unsafe.Add(ref destinationBase, i * 48);
StoreRgb24Batch(Vector128.ShuffleNative(rgba0, removeAlpha), ref destination0);
StoreRgb24Batch(Vector128.ShuffleNative(rgba1, removeAlpha), ref Unsafe.Add(ref destination0, 12));
StoreRgb24Batch(Vector128.ShuffleNative(rgba2, removeAlpha), ref Unsafe.Add(ref destination0, 24));
StoreRgb24Batch(Vector128.ShuffleNative(rgba3, removeAlpha), ref Unsafe.Add(ref destination0, 36));
}
int convertedCount = (int)(batchCount * (uint)Vector128<byte>.Count);
redChannel = redChannel[convertedCount..];
greenChannel = greenChannel[convertedCount..];
blueChannel = blueChannel[convertedCount..];
destination = destination[convertedCount..];
}
/// <summary>
/// Packs complete sixteen-pixel batches into exact-length <see cref="Rgba32"/> storage using portable 128-bit SIMD.
/// </summary>
/// <param name="redChannel">The red source span, advanced past the converted batches.</param>
/// <param name="greenChannel">The green source span, advanced past the converted batches.</param>
/// <param name="blueChannel">The blue source span, advanced past the converted batches.</param>
/// <param name="destination">The destination span, advanced past the converted batches.</param>
private static void PackFromRgbPlanesVector128Reduce(
ref ReadOnlySpan<byte> redChannel,
ref ReadOnlySpan<byte> greenChannel,
ref ReadOnlySpan<byte> blueChannel,
ref Span<Rgba32> destination)
{
ref byte redBase = ref MemoryMarshal.GetReference(redChannel);
ref byte greenBase = ref MemoryMarshal.GetReference(greenChannel);
ref byte blueBase = ref MemoryMarshal.GetReference(blueChannel);
ref Vector128<byte> destinationBase = ref Unsafe.As<Rgba32, Vector128<byte>>(ref MemoryMarshal.GetReference(destination));
Vector128<byte> opaqueAlpha = Vector128.Create(byte.MaxValue);
nuint batchCount = (nuint)(uint)redChannel.Length / (uint)Vector128<byte>.Count;
for (nuint i = 0; i < batchCount; i++)
{
nuint sourceOffset = i * (uint)Vector128<byte>.Count;
Vector128<byte> red = Vector128.LoadUnsafe(ref redBase, sourceOffset);
Vector128<byte> green = Vector128.LoadUnsafe(ref greenBase, sourceOffset);
Vector128<byte> blue = Vector128.LoadUnsafe(ref blueBase, sourceOffset);
InterleaveRgbPlanes(red, green, blue, opaqueAlpha, out Vector128<byte> rgba0, out Vector128<byte> rgba1, out Vector128<byte> rgba2, out Vector128<byte> rgba3);
ref Vector128<byte> destination0 = ref Unsafe.Add(ref destinationBase, i * 4);
destination0 = rgba0;
Unsafe.Add(ref destination0, 1) = rgba1;
Unsafe.Add(ref destination0, 2) = rgba2;
Unsafe.Add(ref destination0, 3) = rgba3;
}
int convertedCount = (int)(batchCount * (uint)Vector128<byte>.Count);
redChannel = redChannel[convertedCount..];
greenChannel = greenChannel[convertedCount..];
blueChannel = blueChannel[convertedCount..];
destination = destination[convertedCount..];
}
/// <summary>
/// Interleaves sixteen planar RGB samples into four groups of four opaque RGBA pixels.
/// </summary>
/// <param name="red">The red component lanes.</param>
/// <param name="green">The green component lanes.</param>
/// <param name="blue">The blue component lanes.</param>
/// <param name="alpha">The opaque alpha lanes.</param>
/// <param name="rgba0">The first four interleaved pixels.</param>
/// <param name="rgba1">The second four interleaved pixels.</param>
/// <param name="rgba2">The third four interleaved pixels.</param>
/// <param name="rgba3">The fourth four interleaved pixels.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InterleaveRgbPlanes(
Vector128<byte> red,
Vector128<byte> green,
Vector128<byte> blue,
Vector128<byte> alpha,
out Vector128<byte> rgba0,
out Vector128<byte> rgba1,
out Vector128<byte> rgba2,
out Vector128<byte> rgba3)
{
Vector128<byte> redGreenLow = Vector128_.UnpackLow(red, green);
Vector128<byte> redGreenHigh = Vector128_.UnpackHigh(red, green);
Vector128<byte> blueAlphaLow = Vector128_.UnpackLow(blue, alpha);
Vector128<byte> blueAlphaHigh = Vector128_.UnpackHigh(blue, alpha);
rgba0 = Vector128_.UnpackLow(redGreenLow.AsInt16(), blueAlphaLow.AsInt16()).AsByte();
rgba1 = Vector128_.UnpackHigh(redGreenLow.AsInt16(), blueAlphaLow.AsInt16()).AsByte();
rgba2 = Vector128_.UnpackLow(redGreenHigh.AsInt16(), blueAlphaHigh.AsInt16()).AsByte();
rgba3 = Vector128_.UnpackHigh(redGreenHigh.AsInt16(), blueAlphaHigh.AsInt16()).AsByte();
}
/// <summary>
/// Stores the twelve packed RGB bytes in one shuffled SIMD value without writing its unused lanes.
/// </summary>
/// <param name="value">The packed RGB bytes in the first twelve lanes.</param>
/// <param name="destination">The first destination byte.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void StoreRgb24Batch(Vector128<byte> value, ref byte destination)
{
Unsafe.WriteUnaligned(ref destination, value.AsUInt64().ToScalar());
Unsafe.WriteUnaligned(ref Unsafe.Add(ref destination, 8), value.AsUInt32().GetElement(2));
}
private static void PackFromRgbPlanesScalarBatchedReduce(
ref ReadOnlySpan<byte> redChannel,
ref ReadOnlySpan<byte> greenChannel,
@ -86,8 +229,8 @@ internal static partial class SimdUtils
ref ByteTuple4 b = ref Unsafe.As<byte, ByteTuple4>(ref MemoryMarshal.GetReference(blueChannel));
ref Rgb24 rgb = ref MemoryMarshal.GetReference(destination);
nuint count = (uint)redChannel.Length / 4;
for (nuint i = 0; i < count; i++)
nuint batchCount = (uint)redChannel.Length / 4;
for (nuint i = 0; i < batchCount; i++)
{
ref Rgb24 d0 = ref Unsafe.Add(ref rgb, i * 4);
ref Rgb24 d1 = ref Unsafe.Add(ref d0, 1);
@ -115,11 +258,11 @@ internal static partial class SimdUtils
d3.B = bb.V3;
}
int finished = (int)(count * 4);
redChannel = redChannel[finished..];
greenChannel = greenChannel[finished..];
blueChannel = blueChannel[finished..];
destination = destination[finished..];
int convertedCount = (int)(batchCount * 4);
redChannel = redChannel[convertedCount..];
greenChannel = greenChannel[convertedCount..];
blueChannel = blueChannel[convertedCount..];
destination = destination[convertedCount..];
}
private static void PackFromRgbPlanesScalarBatchedReduce(
@ -133,9 +276,9 @@ internal static partial class SimdUtils
ref ByteTuple4 b = ref Unsafe.As<byte, ByteTuple4>(ref MemoryMarshal.GetReference(blueChannel));
ref Rgba32 rgb = ref MemoryMarshal.GetReference(destination);
nuint count = (uint)redChannel.Length / 4;
nuint batchCount = (uint)redChannel.Length / 4;
destination.Fill(new Rgba32(0, 0, 0, 255));
for (nuint i = 0; i < count; i++)
for (nuint i = 0; i < batchCount; i++)
{
ref Rgba32 d0 = ref Unsafe.Add(ref rgb, i * 4);
ref Rgba32 d1 = ref Unsafe.Add(ref d0, 1);
@ -163,11 +306,11 @@ internal static partial class SimdUtils
d3.B = bb.V3;
}
int finished = (int)(count * 4);
redChannel = redChannel[finished..];
greenChannel = greenChannel[finished..];
blueChannel = blueChannel[finished..];
destination = destination[finished..];
int convertedCount = (int)(batchCount * 4);
redChannel = redChannel[convertedCount..];
greenChannel = greenChannel[convertedCount..];
blueChannel = blueChannel[convertedCount..];
destination = destination[convertedCount..];
}
private static void PackFromRgbPlanesRemainder(
@ -181,7 +324,7 @@ internal static partial class SimdUtils
ref byte b = ref MemoryMarshal.GetReference(blueChannel);
ref Rgb24 rgb = ref MemoryMarshal.GetReference(destination);
for (nuint i = 0; i < (uint)destination.Length; i++)
for (nuint i = 0; i < (uint)redChannel.Length; i++)
{
ref Rgb24 d = ref Unsafe.Add(ref rgb, i);
d.R = Unsafe.Add(ref r, i);
@ -201,7 +344,7 @@ internal static partial class SimdUtils
ref byte b = ref MemoryMarshal.GetReference(blueChannel);
ref Rgba32 rgba = ref MemoryMarshal.GetReference(destination);
for (nuint i = 0; i < (uint)destination.Length; i++)
for (nuint i = 0; i < (uint)redChannel.Length; i++)
{
ref Rgba32 d = ref Unsafe.Add(ref rgba, i);
d.R = Unsafe.Add(ref r, i);
@ -234,4 +377,291 @@ internal static partial class SimdUtils
Unsafe.Add(ref b, i) = src.B;
}
}
/// <summary>
/// Provides the hardware-intrinsic reducers used by the planar RGB packing pipeline.
/// </summary>
public static partial class HwIntrinsics
{
/// <summary>
/// Creates the AVX2 lane order used before interleaving planar RGB components.
/// </summary>
/// <returns>The source lane permutation.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<uint> PermuteMaskEvenOdd8x32() => Vector256.Create(0u, 2, 4, 6, 1, 3, 5, 7);
/// <summary>
/// Packs complete AVX2 batches into <see cref="Rgb24"/> pixels and retains the unconverted remainder.
/// </summary>
/// <param name="redChannel">The red source span.</param>
/// <param name="greenChannel">The green source span.</param>
/// <param name="blueChannel">The blue source span.</param>
/// <param name="destination">The destination pixel span.</param>
internal static void PackFromRgbPlanesReduce(
ref ReadOnlySpan<byte> redChannel,
ref ReadOnlySpan<byte> greenChannel,
ref ReadOnlySpan<byte> blueChannel,
ref Span<Rgb24> destination)
{
ref Vector256<byte> redBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(redChannel));
ref Vector256<byte> greenBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(greenChannel));
ref Vector256<byte> blueBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(blueChannel));
ref byte destinationBase = ref Unsafe.As<Rgb24, byte>(ref MemoryMarshal.GetReference(destination));
nuint batchCount = redChannel.Vector256Count<byte>();
Vector256<uint> sourceOrder = PermuteMaskEvenOdd8x32();
Vector256<uint> packedOrder = Vector256.Create(0u, 1, 2, 4, 5, 6, 3, 7);
Vector256<byte> opaqueAlpha = Vector256.Create(byte.MaxValue);
Vector128<byte> removeAlphaLower = Vector128.Create((byte)0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 3, 7, 11, 15);
Vector128<byte> removeAlphaUpper = Vector128.Create((byte)16, 17, 18, 20, 21, 22, 24, 25, 26, 28, 29, 30, 19, 23, 27, 31);
Vector256<byte> removeAlpha = Vector256.Create(removeAlphaLower, removeAlphaUpper);
bool hasWritablePadding = destination.Length >= redChannel.Length + 3;
nuint i = 0;
// Non-final batches retain the original four overlapping wide stores. Splitting the final batch keeps
// the exact-row decision out of the hot loop and limits the narrower stores to the only bytes that can
// cross the destination boundary.
for (; i + 1 < batchCount; i++)
{
PackRgb24Batch(
Unsafe.Add(ref redBase, i),
Unsafe.Add(ref greenBase, i),
Unsafe.Add(ref blueBase, i),
opaqueAlpha,
sourceOrder,
packedOrder,
removeAlpha,
out Vector256<byte> rgb0,
out Vector256<byte> rgb1,
out Vector256<byte> rgb2,
out Vector256<byte> rgb3);
ref byte destination0 = ref Unsafe.Add(ref destinationBase, 96 * i);
ref byte destination1 = ref Unsafe.Add(ref destination0, 24);
ref byte destination2 = ref Unsafe.Add(ref destination1, 24);
ref byte destination3 = ref Unsafe.Add(ref destination2, 24);
Unsafe.As<byte, Vector256<byte>>(ref destination0) = rgb0;
Unsafe.As<byte, Vector256<byte>>(ref destination1) = rgb1;
Unsafe.As<byte, Vector256<byte>>(ref destination2) = rgb2;
Unsafe.As<byte, Vector256<byte>>(ref destination3) = rgb3;
}
if (i < batchCount)
{
PackRgb24Batch(
Unsafe.Add(ref redBase, i),
Unsafe.Add(ref greenBase, i),
Unsafe.Add(ref blueBase, i),
opaqueAlpha,
sourceOrder,
packedOrder,
removeAlpha,
out Vector256<byte> rgb0,
out Vector256<byte> rgb1,
out Vector256<byte> rgb2,
out Vector256<byte> rgb3);
ref byte destination0 = ref Unsafe.Add(ref destinationBase, 96 * i);
ref byte destination1 = ref Unsafe.Add(ref destination0, 24);
ref byte destination2 = ref Unsafe.Add(ref destination1, 24);
ref byte destination3 = ref Unsafe.Add(ref destination2, 24);
Unsafe.As<byte, Vector256<byte>>(ref destination0) = rgb0;
Unsafe.As<byte, Vector256<byte>>(ref destination1) = rgb1;
Unsafe.As<byte, Vector256<byte>>(ref destination2) = rgb2;
if (hasWritablePadding)
{
Unsafe.As<byte, Vector256<byte>>(ref destination3) = rgb3;
}
else
{
// The final compacted vector contains 24 RGB bytes followed by eight unused bytes. Exact stores
// retain all useful bytes without writing beyond an unpadded destination row.
Unsafe.As<byte, Vector128<byte>>(ref destination3) = rgb3.GetLower();
Unsafe.As<byte, ulong>(ref Unsafe.Add(ref destination3, 16)) = rgb3.GetUpper().AsUInt64().ToScalar();
}
}
int convertedCount = (int)batchCount * Vector256<byte>.Count;
redChannel = redChannel[convertedCount..];
greenChannel = greenChannel[convertedCount..];
blueChannel = blueChannel[convertedCount..];
destination = destination[convertedCount..];
}
/// <summary>
/// Packs complete AVX2 batches into <see cref="Rgba32"/> pixels and retains the unconverted remainder.
/// </summary>
/// <param name="redChannel">The red source span.</param>
/// <param name="greenChannel">The green source span.</param>
/// <param name="blueChannel">The blue source span.</param>
/// <param name="destination">The destination pixel span.</param>
internal static void PackFromRgbPlanesReduce(
ref ReadOnlySpan<byte> redChannel,
ref ReadOnlySpan<byte> greenChannel,
ref ReadOnlySpan<byte> blueChannel,
ref Span<Rgba32> destination)
{
ref Vector256<byte> redBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(redChannel));
ref Vector256<byte> greenBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(greenChannel));
ref Vector256<byte> blueBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(blueChannel));
ref Vector256<byte> destinationBase = ref Unsafe.As<Rgba32, Vector256<byte>>(ref MemoryMarshal.GetReference(destination));
nuint batchCount = redChannel.Vector256Count<byte>();
Vector256<uint> sourceOrder = PermuteMaskEvenOdd8x32();
Vector256<byte> opaqueAlpha = Vector256.Create(byte.MaxValue);
for (nuint i = 0; i < batchCount; i++)
{
InterleaveRgbPlanes(
Unsafe.Add(ref redBase, i),
Unsafe.Add(ref greenBase, i),
Unsafe.Add(ref blueBase, i),
opaqueAlpha,
sourceOrder,
out Vector256<byte> rgba0,
out Vector256<byte> rgba1,
out Vector256<byte> rgba2,
out Vector256<byte> rgba3);
ref Vector256<byte> destination0 = ref Unsafe.Add(ref destinationBase, i * 4);
destination0 = rgba0;
Unsafe.Add(ref destination0, 1) = rgba1;
Unsafe.Add(ref destination0, 2) = rgba2;
Unsafe.Add(ref destination0, 3) = rgba3;
}
int convertedCount = (int)batchCount * Vector256<byte>.Count;
redChannel = redChannel[convertedCount..];
greenChannel = greenChannel[convertedCount..];
blueChannel = blueChannel[convertedCount..];
destination = destination[convertedCount..];
}
/// <summary>
/// Unpacks complete AVX2 batches from <see cref="Rgb24"/> pixels and retains the unconverted remainder.
/// </summary>
/// <param name="redChannel">The red destination span.</param>
/// <param name="greenChannel">The green destination span.</param>
/// <param name="blueChannel">The blue destination span.</param>
/// <param name="source">The source pixel span.</param>
internal static void UnpackToRgbPlanesReduce(
ref Span<float> redChannel,
ref Span<float> greenChannel,
ref Span<float> blueChannel,
ref ReadOnlySpan<Rgb24> source)
{
ref Vector256<byte> sourceBase = ref Unsafe.As<Rgb24, Vector256<byte>>(ref MemoryMarshal.GetReference(source));
ref Vector256<float> redBase = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(redChannel));
ref Vector256<float> greenBase = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(greenChannel));
ref Vector256<float> blueBase = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(blueChannel));
Vector256<uint> separateLanes = Vector256.Create(0u, 1, 2, 6, 3, 4, 5, 7);
Vector128<byte> extractRgbLower = Vector128.Create((byte)0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11, byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue);
Vector128<byte> extractRgbUpper = Vector128.Create((byte)16, 19, 22, 25, 17, 20, 23, 26, 18, 21, 24, 27, byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue);
Vector256<byte> extractRgb = Vector256.Create(extractRgbLower, extractRgbUpper);
// Each iteration consumes eight Rgb24 pixels, or 24 bytes, but starts with a 32-byte load. Three extra
// source pixels must therefore remain addressable beyond every vectorized batch.
const int bytesPerBatch = 24;
nuint batchCount = source.Length > 3 ? (uint)(source.Length - 3) / 8 : 0;
for (nuint i = 0; i < batchCount; i++)
{
Vector256<byte> packed = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref sourceBase, (uint)(bytesPerBatch * i)).AsUInt32(), separateLanes).AsByte();
packed = Vector256.ShuffleNative(packed, extractRgb);
Vector256<byte> redGreen = Avx2.UnpackLow(packed, Vector256<byte>.Zero);
Vector256<byte> blue = Avx2.UnpackHigh(packed, Vector256<byte>.Zero);
Vector256<float> red = Avx.ConvertToVector256Single(Avx2.UnpackLow(redGreen, Vector256<byte>.Zero).AsInt32());
Vector256<float> green = Avx.ConvertToVector256Single(Avx2.UnpackHigh(redGreen, Vector256<byte>.Zero).AsInt32());
Vector256<float> blueValues = Avx.ConvertToVector256Single(Avx2.UnpackLow(blue, Vector256<byte>.Zero).AsInt32());
Unsafe.Add(ref redBase, i) = red;
Unsafe.Add(ref greenBase, i) = green;
Unsafe.Add(ref blueBase, i) = blueValues;
}
int convertedCount = (int)(batchCount * 8);
redChannel = redChannel[convertedCount..];
greenChannel = greenChannel[convertedCount..];
blueChannel = blueChannel[convertedCount..];
source = source[convertedCount..];
}
/// <summary>
/// Interleaves and compacts one AVX2 batch into four groups of eight <see cref="Rgb24"/> pixels.
/// </summary>
/// <param name="red">The red component lanes.</param>
/// <param name="green">The green component lanes.</param>
/// <param name="blue">The blue component lanes.</param>
/// <param name="alpha">The opaque alpha lanes used during interleaving.</param>
/// <param name="sourceOrder">The cross-lane source permutation.</param>
/// <param name="packedOrder">The cross-lane packed RGB permutation.</param>
/// <param name="removeAlpha">The native byte-shuffle indices that compact RGBA to RGB.</param>
/// <param name="rgb0">The first eight packed pixels.</param>
/// <param name="rgb1">The second eight packed pixels.</param>
/// <param name="rgb2">The third eight packed pixels.</param>
/// <param name="rgb3">The fourth eight packed pixels.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void PackRgb24Batch(
Vector256<byte> red,
Vector256<byte> green,
Vector256<byte> blue,
Vector256<byte> alpha,
Vector256<uint> sourceOrder,
Vector256<uint> packedOrder,
Vector256<byte> removeAlpha,
out Vector256<byte> rgb0,
out Vector256<byte> rgb1,
out Vector256<byte> rgb2,
out Vector256<byte> rgb3)
{
InterleaveRgbPlanes(red, green, blue, alpha, sourceOrder, out Vector256<byte> rgba0, out Vector256<byte> rgba1, out Vector256<byte> rgba2, out Vector256<byte> rgba3);
rgb0 = Avx2.PermuteVar8x32(Vector256.ShuffleNative(rgba0, removeAlpha).AsUInt32(), packedOrder).AsByte();
rgb1 = Avx2.PermuteVar8x32(Vector256.ShuffleNative(rgba1, removeAlpha).AsUInt32(), packedOrder).AsByte();
rgb2 = Avx2.PermuteVar8x32(Vector256.ShuffleNative(rgba2, removeAlpha).AsUInt32(), packedOrder).AsByte();
rgb3 = Avx2.PermuteVar8x32(Vector256.ShuffleNative(rgba3, removeAlpha).AsUInt32(), packedOrder).AsByte();
}
/// <summary>
/// Interleaves 32 planar RGB samples into four groups of eight opaque RGBA pixels.
/// </summary>
/// <param name="red">The red component lanes.</param>
/// <param name="green">The green component lanes.</param>
/// <param name="blue">The blue component lanes.</param>
/// <param name="alpha">The opaque alpha lanes.</param>
/// <param name="sourceOrder">The cross-lane source permutation.</param>
/// <param name="rgba0">The first eight interleaved pixels.</param>
/// <param name="rgba1">The second eight interleaved pixels.</param>
/// <param name="rgba2">The third eight interleaved pixels.</param>
/// <param name="rgba3">The fourth eight interleaved pixels.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InterleaveRgbPlanes(
Vector256<byte> red,
Vector256<byte> green,
Vector256<byte> blue,
Vector256<byte> alpha,
Vector256<uint> sourceOrder,
out Vector256<byte> rgba0,
out Vector256<byte> rgba1,
out Vector256<byte> rgba2,
out Vector256<byte> rgba3)
{
red = Avx2.PermuteVar8x32(red.AsUInt32(), sourceOrder).AsByte();
green = Avx2.PermuteVar8x32(green.AsUInt32(), sourceOrder).AsByte();
blue = Avx2.PermuteVar8x32(blue.AsUInt32(), sourceOrder).AsByte();
Vector256<byte> redGreenLow = Avx2.UnpackLow(red, green);
Vector256<byte> redGreenHigh = Avx2.UnpackHigh(red, green);
Vector256<byte> blueAlphaLow = Avx2.UnpackLow(blue, alpha);
Vector256<byte> blueAlphaHigh = Avx2.UnpackHigh(blue, alpha);
rgba0 = Avx2.UnpackLow(redGreenLow.AsUInt16(), blueAlphaLow.AsUInt16()).AsByte();
rgba1 = Avx2.UnpackHigh(redGreenLow.AsUInt16(), blueAlphaLow.AsUInt16()).AsByte();
rgba2 = Avx2.UnpackLow(redGreenHigh.AsUInt16(), blueAlphaHigh.AsUInt16()).AsByte();
rgba3 = Avx2.UnpackHigh(redGreenHigh.AsUInt16(), blueAlphaHigh.AsUInt16()).AsByte();
}
}
}

3
src/ImageSharp/Configuration.cs

@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Pbm;
@ -225,6 +226,7 @@ public sealed class Configuration
/// <see cref="WebpConfigurationModule"/>.
/// <see cref="ExrConfigurationModule"/>.
/// <see cref="QoiConfigurationModule"/>.
/// <see cref="HeifConfigurationModule"/>.
/// <see cref="AniConfigurationModule"/>.
/// </summary>
/// <returns>The default configuration of <see cref="Configuration"/>.</returns>
@ -239,6 +241,7 @@ public sealed class Configuration
new WebpConfigurationModule(),
new ExrConfigurationModule(),
new QoiConfigurationModule(),
new HeifConfigurationModule(),
new IcoConfigurationModule(),
new CurConfigurationModule(),
new AniConfigurationModule());

2
src/ImageSharp/Formats/DecoderOptions.cs

@ -78,7 +78,7 @@ public sealed class DecoderOptions
return false;
}
if (this.ColorProfileHandling == ColorProfileHandling.Preserve)
if (this.ColorProfileHandling != ColorProfileHandling.Convert)
{
return false;
}

25
src/ImageSharp/Formats/Heif/Av1/Av1BitDepth.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Identifies the sample precision of an AV1 sequence.
/// </summary>
internal enum Av1BitDepth : int
{
/// <summary>
/// Eight bits per sample.
/// </summary>
EightBit = 0,
/// <summary>
/// Ten bits per sample.
/// </summary>
TenBit = 1,
/// <summary>
/// Twelve bits per sample.
/// </summary>
TwelveBit = 2,
}

19
src/ImageSharp/Formats/Heif/Av1/Av1BitDepthExtensions.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Provides sample-precision conversions for AV1 bit-depth values.
/// </summary>
internal static class Av1BitDepthExtensions
{
/// <summary>
/// Gets the number of bits represented by an AV1 bit-depth value.
/// </summary>
/// <param name="bitDepth">The AV1 bit-depth value.</param>
/// <returns>Eight, ten, or twelve.</returns>
public static int GetBitCount(this Av1BitDepth bitDepth) => 8 + ((int)bitDepth << 1);
}

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

@ -0,0 +1,314 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Reads AV1 fixed-width and variable-length syntax from a most-significant-bit-first byte span.
/// </summary>
internal ref struct Av1BitStreamReader
{
/// <summary>
/// The complete encoded byte span.
/// </summary>
private readonly Span<byte> data;
/// <summary>
/// Initializes a new instance of the <see cref="Av1BitStreamReader"/> struct.
/// </summary>
/// <param name="data">The encoded AV1 data.</param>
public Av1BitStreamReader(Span<byte> data) => this.data = data;
/// <summary>
/// Gets the zero-based position of the next bit to read.
/// </summary>
public int BitPosition { get; private set; } = 0;
/// <summary>
/// Gets the number of bytes in the reader's buffer.
/// </summary>
public readonly int Length => this.data.Length;
/// <summary>
/// Moves the next read position to the beginning of the buffer.
/// </summary>
public void Reset() => this.BitPosition = 0;
/// <summary>
/// Advances the read position without interpreting the skipped bits.
/// </summary>
/// <param name="bitCount">The number of bits to skip.</param>
public void Skip(int bitCount) => this.BitPosition += bitCount;
/// <summary>
/// Reads an unsigned fixed-width value in most-significant-bit-first order.
/// </summary>
/// <param name="bitCount">The number of bits to read.</param>
/// <returns>The decoded unsigned value.</returns>
public uint ReadLiteral(int bitCount)
{
DebugGuard.MustBeBetweenOrEqualTo(bitCount, 0, 32, nameof(bitCount));
uint literal = 0;
for (int bit = bitCount - 1; bit >= 0; bit--)
{
literal |= this.ReadBit() << bit;
}
return literal;
}
/// <summary>
/// Reads the next encoded bit.
/// </summary>
/// <returns>Zero or one.</returns>
public uint ReadBit()
{
int byteOffset = Av1Math.DivideBy8Floor(this.BitPosition);
byte shift = (byte)(7 - Av1Math.Modulus8(this.BitPosition));
this.BitPosition++;
return (uint)((this.data[byteOffset] >> shift) & 0x01);
}
/// <summary>
/// Reads the next encoded bit as a Boolean value.
/// </summary>
/// <returns><see langword="true"/> for one; otherwise, <see langword="false"/>.</returns>
public bool ReadBoolean() => this.ReadLiteral(1) > 0;
/// <summary>
/// Reads an AV1 little-endian base-128 value from a byte-aligned position.
/// </summary>
/// <param name="length">Receives the number of encoded bytes consumed.</param>
/// <returns>The decoded unsigned value.</returns>
public ulong ReadLittleEndianBytes128(out int length)
{
DebugGuard.IsTrue((this.BitPosition & 0x07) == 0, $"Reading of Little Endian 128 value only allowed on byte alignment (offset {this.BitPosition}).");
ulong value = 0;
length = 0;
for (int shift = 0; shift < 56; shift += 7)
{
uint leb128Byte = this.ReadLiteral(8);
value |= (leb128Byte & 0x7FUL) << shift;
length++;
if ((leb128Byte & 0x80U) == 0)
{
return value;
}
}
// AV1 limits unsigned LEB128 fields to eight bytes. A continuation bit in the eighth byte does not describe
// another value byte; accepting it would move the following OBU header into the declared size field.
throw new InvalidImageContentException("The AV1 LEB128 value is not terminated within eight bytes.");
}
/// <summary>
/// Reads the AV1 unsigned-variable-length code.
/// </summary>
/// <returns>The decoded unsigned value.</returns>
public uint ReadUnsignedVariableLength()
{
int leadingZerosCount = 0;
while (leadingZerosCount < 32)
{
uint bit = this.ReadLiteral(1);
if (bit == 1)
{
break;
}
leadingZerosCount++;
}
if (leadingZerosCount == 32)
{
return uint.MaxValue;
}
if (leadingZerosCount != 0)
{
uint basis = (1U << leadingZerosCount) - 1U;
uint value = this.ReadLiteral(leadingZerosCount);
return basis + value;
}
return 0;
}
/// <summary>
/// Reads a value from an alphabet whose size is not a power of two.
/// </summary>
/// <param name="n">The number of symbols in the alphabet.</param>
/// <returns>A decoded symbol in the range zero through <paramref name="n"/> minus one.</returns>
public uint ReadNonSymmetric(uint n)
{
if (n <= 1)
{
return 0;
}
int w = (int)(Av1Math.FloorLog2(n) + 1);
uint m = (uint)((1 << w) - n);
uint v = this.ReadLiteral(w - 1);
if (v < m)
{
return v;
}
return (v << 1) - m + this.ReadLiteral(1);
}
/// <summary>
/// Reads a finite subexponential value recentered around a signed reference value.
/// </summary>
/// <param name="valueMagnitude">One greater than the maximum absolute value in the signed domain.</param>
/// <param name="groupBitCount">The bit width of the first subexponential group.</param>
/// <param name="reference">The signed reference value around which smaller codewords are concentrated.</param>
/// <returns>A decoded value in the inclusive range from minus <paramref name="valueMagnitude"/> plus one through
/// <paramref name="valueMagnitude"/> minus one.</returns>
public int ReadSignedReferenceSubexponential(int valueMagnitude, int groupBitCount, int reference)
{
int shiftedReference = reference + valueMagnitude - 1;
int scaledValueCount = (valueMagnitude << 1) - 1;
return this.ReadReferenceSubexponential(scaledValueCount, groupBitCount, shiftedReference) - valueMagnitude + 1;
}
/// <summary>
/// Reads a fixed-width two's-complement signed integer.
/// </summary>
/// <param name="n">The encoded bit width.</param>
/// <returns>The sign-extended integer.</returns>
public int ReadSignedFromUnsigned(int n)
{
int signedValue;
uint value = this.ReadLiteral(n);
uint signMask = 1U << (n - 1);
if ((value & signMask) == signMask)
{
// The subtraction represents sign extension; widening first preserves the n=32 case.
signedValue = (int)((long)value - (signMask << 1));
}
else
{
signedValue = (int)value;
}
return signedValue;
}
/// <summary>
/// Reads a byte-aligned unsigned integer whose least-significant byte is encoded first.
/// </summary>
/// <param name="n">The number of bytes to read.</param>
/// <returns>The decoded unsigned integer.</returns>
public uint ReadLittleEndian(int n)
{
DebugGuard.IsTrue(Av1Math.Modulus8(this.BitPosition) == 0, "Reading of Little Endian value only allowed on byte alignment");
uint t = 0;
for (int i = 0; i < 8 * n; i += 8)
{
t += this.ReadLiteral(8) << i;
}
return t;
}
/// <summary>
/// Gets a byte-aligned tile payload for entropy decoding and advances past it.
/// </summary>
/// <param name="tileDataSize">The tile payload length in bytes.</param>
/// <returns>The tile payload span.</returns>
public Span<byte> GetSymbolReader(int tileDataSize)
=> this.ReadBytes(tileDataSize);
/// <summary>
/// Gets the next byte-aligned portion of the encoded data and advances past it.
/// </summary>
/// <param name="byteCount">The number of bytes to read.</param>
/// <returns>The requested bytes.</returns>
public Span<byte> ReadBytes(int byteCount)
{
DebugGuard.IsTrue(Av1Math.Modulus8(this.BitPosition) == 0, "Byte spans must start on a byte boundary.");
int byteOffset = Av1Math.DivideBy8Floor(this.BitPosition);
if ((uint)byteOffset > (uint)this.data.Length || (uint)byteCount > (uint)(this.data.Length - byteOffset))
{
throw new InvalidImageContentException("The AV1 payload exceeds its declared data boundary.");
}
Span<byte> payload = this.data.Slice(byteOffset, byteCount);
this.Skip(byteCount << 3);
return payload;
}
/// <summary>
/// Reads a finite subexponential value and inverse-recenters it around an unsigned reference value.
/// </summary>
/// <param name="valueCount">The number of values in the finite domain.</param>
/// <param name="groupBitCount">The bit width of the first subexponential group.</param>
/// <param name="reference">The reference value within the finite domain.</param>
/// <returns>The decoded value in the range zero through <paramref name="valueCount"/> minus one.</returns>
private int ReadReferenceSubexponential(int valueCount, int groupBitCount, int reference)
{
int value = this.ReadSubexponential(valueCount, groupBitCount);
// Recentering enumerates values by increasing distance from the reference. References in the upper half use
// the mirrored domain so the shorter side of the finite range always participates in the alternating mapping.
if ((reference << 1) <= valueCount)
{
return InverseRecenter(reference, value);
}
return valueCount - 1 - InverseRecenter(valueCount - 1 - reference, value);
}
/// <summary>
/// Reads one value from a finite subexponential code.
/// </summary>
/// <param name="valueCount">The number of values in the finite domain.</param>
/// <param name="groupBitCount">The bit width of the first subexponential group.</param>
/// <returns>The decoded zero-based value.</returns>
private int ReadSubexponential(int valueCount, int groupBitCount)
{
int groupIndex = 0;
int groupStart = 0;
while (true)
{
// AV1 keeps the first two groups at width k and then doubles each following group. Once fewer than three
// groups remain, the non-symmetric code consumes the exact finite tail without introducing unused values.
int bitCount = groupIndex == 0 ? groupBitCount : groupBitCount + groupIndex - 1;
int groupSize = 1 << bitCount;
if (valueCount <= groupStart + (3 * groupSize))
{
return (int)this.ReadNonSymmetric((uint)(valueCount - groupStart)) + groupStart;
}
if (!this.ReadBoolean())
{
return (int)this.ReadLiteral(bitCount) + groupStart;
}
groupIndex++;
groupStart += groupSize;
}
}
/// <summary>
/// Maps a nonnegative code value around a nonnegative reference value.
/// </summary>
/// <param name="reference">The recentering reference.</param>
/// <param name="value">The coded nonnegative value.</param>
/// <returns>The inverse-recentered value.</returns>
private static int InverseRecenter(int reference, int value)
{
// Codes within twice the reference alternate above and below it: even values select the upper side and odd
// values select the lower side. Larger codes lie beyond the lower-side range and map directly to the tail.
if (value > (reference << 1))
{
return value;
}
return (value & 1) == 0 ? (value >> 1) + reference : reference - ((value + 1) >> 1);
}
}

266
src/ImageSharp/Formats/Heif/Av1/Av1BitStreamWriter.cs

@ -0,0 +1,266 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Writes AV1 fixed-width and variable-length syntax to reusable expanding memory.
/// </summary>
internal ref struct Av1BitStreamWriter
{
/// <summary>
/// The number of bits in one output byte.
/// </summary>
private const int WordSize = 8;
/// <summary>
/// The expanding output allocation.
/// </summary>
private readonly AutoExpandingMemory<byte> memory;
/// <summary>
/// The current writable view over <see cref="memory"/>.
/// </summary>
private Span<byte> span;
/// <summary>
/// The final byte index that can be written without expanding <see cref="memory"/>.
/// </summary>
private int capacityTrigger;
/// <summary>
/// The partially assembled output byte.
/// </summary>
private byte buffer = 0;
/// <summary>
/// Initializes a new instance of the <see cref="Av1BitStreamWriter"/> struct.
/// </summary>
/// <param name="memory">The reusable expanding output allocation.</param>
public Av1BitStreamWriter(AutoExpandingMemory<byte> memory)
{
this.memory = memory;
this.span = memory.GetEntireSpan();
this.capacityTrigger = memory.Capacity - 1;
}
/// <summary>
/// Gets the zero-based position of the next output bit.
/// </summary>
public int BitPosition { get; private set; } = 0;
/// <summary>
/// Gets the current output capacity in bytes.
/// </summary>
public readonly int Capacity => this.memory.Capacity;
/// <summary>
/// Encodes an unsigned 32-bit value using little-endian base-128 bytes.
/// </summary>
/// <param name="value">The value to encode.</param>
/// <param name="span">The destination receiving up to five bytes.</param>
/// <returns>The number of bytes written.</returns>
public static int GetLittleEndianBytes128(uint value, Span<byte> span)
{
int length = 0;
do
{
byte encodedByte = (byte)(value & 0x7fU);
value >>= 7;
if (value != 0)
{
encodedByte |= 0x80;
}
span[length++] = encodedByte;
}
while (value != 0);
return length;
}
/// <summary>
/// Advances the output position, emitting the current byte whenever the skip crosses a byte boundary.
/// </summary>
/// <param name="bitCount">The number of bits to skip.</param>
public void Skip(int bitCount)
{
this.BitPosition += bitCount;
while (this.BitPosition >= WordSize)
{
this.BitPosition -= WordSize;
this.WriteBuffer();
}
}
/// <summary>
/// Writes a partially assembled byte and resets the position for output-memory reuse.
/// </summary>
public void Flush()
{
if (Av1Math.Modulus8(this.BitPosition) != 0)
{
// Flush a partial byte also.
this.WriteBuffer();
}
this.BitPosition = 0;
}
/// <summary>
/// Writes an unsigned fixed-width value in most-significant-bit-first order.
/// </summary>
/// <param name="value">The value to write.</param>
/// <param name="bitCount">The number of low-order bits to write.</param>
public void WriteLiteral(uint value, int bitCount)
{
for (int bit = bitCount - 1; bit >= 0; bit--)
{
this.WriteBit((byte)((value >> bit) & 0x1));
}
}
/// <summary>
/// Writes one Boolean bit.
/// </summary>
/// <param name="value">The Boolean value.</param>
public void WriteBoolean(bool value)
{
byte boolByte = value ? (byte)1 : (byte)0;
this.WriteBit(boolByte);
}
/// <summary>
/// Writes a fixed-width signed integer in two's-complement form.
/// </summary>
/// <param name="signedValue">The signed value.</param>
/// <param name="n">The encoded bit width.</param>
public void WriteSignedFromUnsigned(int signedValue, int n)
{
ulong value = (ulong)signedValue;
if (signedValue < 0)
{
value += 1UL << n;
}
this.WriteLiteral((uint)value, n);
}
/// <summary>
/// Writes an unsigned 32-bit value using little-endian base-128 bytes.
/// </summary>
/// <param name="value">The value to write.</param>
public void WriteLittleEndianBytes128(uint value)
{
int wordPosition = this.BitPosition >> 3;
const int maximumEncodedLength = 5;
if (this.span.Length - wordPosition < maximumEncodedLength)
{
this.memory.GetSpan(wordPosition + maximumEncodedLength);
this.span = this.memory.GetEntireSpan();
this.capacityTrigger = this.span.Length - 1;
}
int bytesWritten = GetLittleEndianBytes128(value, this.span[wordPosition..]);
this.BitPosition += bytesWritten << 3;
}
/// <summary>
/// Writes a value from an alphabet whose size is not a power of two.
/// </summary>
/// <param name="value">The symbol value.</param>
/// <param name="numberOfSymbols">The number of symbols in the alphabet.</param>
public void WriteNonSymmetric(uint value, uint numberOfSymbols)
{
if (numberOfSymbols <= 1)
{
return;
}
int w = (int)(Av1Math.FloorLog2(numberOfSymbols) + 1);
uint m = (uint)((1 << w) - numberOfSymbols);
if (value < m)
{
this.WriteLiteral(value, w - 1);
}
else
{
uint extraBit = ((value + m) >> 1) - value;
uint k = (value + m - extraBit) >> 1;
this.WriteLiteral(k, w - 1);
this.WriteLiteral(extraBit, 1);
}
}
/// <summary>
/// Appends one bit to the partially assembled output byte.
/// </summary>
/// <param name="value">Zero or one.</param>
private void WriteBit(byte value)
{
int bit = this.BitPosition & 0x07;
this.buffer = (byte)(((value << (7 - bit)) & 0xff) | this.buffer);
if (bit == 7)
{
this.WriteBuffer();
}
this.BitPosition++;
}
/// <summary>
/// Writes an unsigned integer with its least-significant byte first.
/// </summary>
/// <param name="value">The value to write.</param>
/// <param name="n">The number of bytes to write.</param>
public void WriteLittleEndian(uint value, int n)
{
DebugGuard.IsTrue(Av1Math.Modulus8(this.BitPosition) == 0, "Writing of Little Endian value only allowed on byte alignment");
uint t = value;
for (int i = 0; i < n; i++)
{
this.WriteLiteral(t & 0xff, 8);
t >>= 8;
}
}
/// <summary>
/// Writes a byte-aligned entropy-coded tile payload.
/// </summary>
/// <param name="tileData">The tile payload.</param>
public void WriteBlob(ReadOnlySpan<byte> tileData)
{
DebugGuard.IsTrue(Av1Math.Modulus8(this.BitPosition) == 0, "Writing of Tile Data only allowed on byte alignment");
int wordPosition = this.BitPosition >> 3;
if (this.span.Length <= wordPosition + tileData.Length)
{
this.memory.GetSpan(wordPosition + tileData.Length);
this.span = this.memory.GetEntireSpan();
}
tileData.CopyTo(this.span[wordPosition..]);
this.BitPosition += tileData.Length << 3;
}
/// <summary>
/// Stores the current output byte, expanding the allocation when necessary.
/// </summary>
private void WriteBuffer()
{
int wordPosition = Av1Math.DivideBy8Floor(this.BitPosition);
if (wordPosition > this.capacityTrigger)
{
// Expand the memory allocation.
this.memory.GetSpan(wordPosition + 1);
this.span = this.memory.GetEntireSpan();
this.capacityTrigger = this.span.Length - 1;
}
this.span[wordPosition] = this.buffer;
this.buffer = 0;
}
}

140
src/ImageSharp/Formats/Heif/Av1/Av1BlockSize.cs

@ -0,0 +1,140 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Identifies every luma block size defined by AV1.
/// </summary>
internal enum Av1BlockSize : byte
{
/// <summary>
/// A block of samples, 4 samples wide and 4 samples high.
/// </summary>
Block4x4 = 0,
/// <summary>
/// A block of samples, 4 samples wide and 8 samples high.
/// </summary>
Block4x8 = 1,
/// <summary>
/// A block of samples, 8 samples wide and 4 samples high.
/// </summary>
Block8x4 = 2,
/// <summary>
/// A block of samples, 8 samples wide and 8 samples high.
/// </summary>
Block8x8 = 3,
/// <summary>
/// A block of samples, 8 samples wide and 16 samples high.
/// </summary>
Block8x16 = 4,
/// <summary>
/// A block of samples, 16 samples wide and 8 samples high.
/// </summary>
Block16x8 = 5,
/// <summary>
/// A block of samples, 16 samples wide and 16 samples high.
/// </summary>
Block16x16 = 6,
/// <summary>
/// A block of samples, 16 samples wide and 32 samples high.
/// </summary>
Block16x32 = 7,
/// <summary>
/// A block of samples, 32 samples wide and 16 samples high.
/// </summary>
Block32x16 = 8,
/// <summary>
/// A block of samples, 32 samples wide and 32 samples high.
/// </summary>
Block32x32 = 9,
/// <summary>
/// A block of samples, 32 samples wide and 64 samples high.
/// </summary>
Block32x64 = 10,
/// <summary>
/// A block of samples, 64 samples wide and 32 samples high.
/// </summary>
Block64x32 = 11,
/// <summary>
/// A block of samples, 64 samples wide and 64 samples high.
/// </summary>
Block64x64 = 12,
/// <summary>
/// A block of samples, 64 samples wide and 128 samples high.
/// </summary>
Block64x128 = 13,
/// <summary>
/// A block of samples, 128 samples wide and 64 samples high.
/// </summary>
Block128x64 = 14,
/// <summary>
/// A block of samples, 128 samples wide and 128 samples high.
/// </summary>
Block128x128 = 15,
/// <summary>
/// A block of samples, 4 samples wide and 16 samples high.
/// </summary>
Block4x16 = 16,
/// <summary>
/// A block of samples, 16 samples wide and 4 samples high.
/// </summary>
Block16x4 = 17,
/// <summary>
/// A block of samples, 8 samples wide and 32 samples high.
/// </summary>
Block8x32 = 18,
/// <summary>
/// A block of samples, 32 samples wide and 8 samples high.
/// </summary>
Block32x8 = 19,
/// <summary>
/// A block of samples, 16 samples wide and 64 samples high.
/// </summary>
Block16x64 = 20,
/// <summary>
/// A block of samples, 64 samples wide and 16 samples high.
/// </summary>
Block64x16 = 21,
/// <summary>
/// The number of concrete block-size values.
/// </summary>
AllSizes = 22,
/// <summary>
/// The first extended rectangular block size following the primary size set.
/// </summary>
SizeS = Block4x16,
/// <summary>
/// A sentinel representing an invalid block size.
/// </summary>
Invalid = 255,
/// <summary>
/// The final value in the primary block-size set.
/// </summary>
Largest = SizeS - 1,
}

222
src/ImageSharp/Formats/Heif/Av1/Av1BlockSizeExtensions.cs

@ -0,0 +1,222 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Provides dimensions, chroma subsampling, and transform limits for AV1 block sizes.
/// </summary>
internal static class Av1BlockSizeExtensions
{
/// <summary>
/// The width of each block size in units of four samples.
/// </summary>
private static readonly int[] SizeWide = [1, 1, 2, 2, 2, 4, 4, 4, 8, 8, 8, 16, 16, 16, 32, 32, 1, 4, 2, 8, 4, 16];
/// <summary>
/// The height of each block size in units of four samples.
/// </summary>
private static readonly int[] SizeHigh = [1, 2, 1, 2, 4, 2, 4, 8, 4, 8, 16, 8, 16, 32, 16, 32, 4, 1, 8, 2, 16, 4];
/// <summary>
/// Maps each luma block size and pair of chroma subsampling shifts to its residual-plane block size.
/// </summary>
private static readonly Av1BlockSize[][][] SubSampled =
[
// ss_x == 0 ss_x == 0 ss_x == 1 ss_x == 1
// ss_y == 0 ss_y == 1 ss_y == 0 ss_y == 1
[[Av1BlockSize.Block4x4, Av1BlockSize.Block4x4], [Av1BlockSize.Block4x4, Av1BlockSize.Block4x4]],
[[Av1BlockSize.Block4x8, Av1BlockSize.Block4x4], [Av1BlockSize.Invalid, Av1BlockSize.Block4x4]],
[[Av1BlockSize.Block8x4, Av1BlockSize.Invalid], [Av1BlockSize.Block4x4, Av1BlockSize.Block4x4]],
[[Av1BlockSize.Block8x8, Av1BlockSize.Block8x4], [Av1BlockSize.Block4x8, Av1BlockSize.Block4x4]],
[[Av1BlockSize.Block8x16, Av1BlockSize.Block8x8], [Av1BlockSize.Invalid, Av1BlockSize.Block4x8]],
[[Av1BlockSize.Block16x8, Av1BlockSize.Invalid], [Av1BlockSize.Block8x8, Av1BlockSize.Block8x4]],
[[Av1BlockSize.Block16x16, Av1BlockSize.Block16x8], [Av1BlockSize.Block8x16, Av1BlockSize.Block8x8]],
[[Av1BlockSize.Block16x32, Av1BlockSize.Block16x16], [Av1BlockSize.Invalid, Av1BlockSize.Block8x16]],
[[Av1BlockSize.Block32x16, Av1BlockSize.Invalid], [Av1BlockSize.Block16x16, Av1BlockSize.Block16x8]],
[[Av1BlockSize.Block32x32, Av1BlockSize.Block32x16], [Av1BlockSize.Block16x32, Av1BlockSize.Block16x16]],
[[Av1BlockSize.Block32x64, Av1BlockSize.Block32x32], [Av1BlockSize.Invalid, Av1BlockSize.Block16x32]],
[[Av1BlockSize.Block64x32, Av1BlockSize.Invalid], [Av1BlockSize.Block32x32, Av1BlockSize.Block32x16]],
[[Av1BlockSize.Block64x64, Av1BlockSize.Block64x32], [Av1BlockSize.Block32x64, Av1BlockSize.Block32x32]],
[[Av1BlockSize.Block64x128, Av1BlockSize.Block64x64], [Av1BlockSize.Invalid, Av1BlockSize.Block32x64]],
[[Av1BlockSize.Block128x64, Av1BlockSize.Invalid], [Av1BlockSize.Block64x64, Av1BlockSize.Block64x32]],
[[Av1BlockSize.Block128x128, Av1BlockSize.Block128x64], [Av1BlockSize.Block64x128, Av1BlockSize.Block64x64]],
[[Av1BlockSize.Block4x16, Av1BlockSize.Block4x8], [Av1BlockSize.Invalid, Av1BlockSize.Block4x8]],
[[Av1BlockSize.Block16x4, Av1BlockSize.Invalid], [Av1BlockSize.Block8x4, Av1BlockSize.Block8x4]],
[[Av1BlockSize.Block8x32, Av1BlockSize.Block8x16], [Av1BlockSize.Invalid, Av1BlockSize.Block4x16]],
[[Av1BlockSize.Block32x8, Av1BlockSize.Invalid], [Av1BlockSize.Block16x8, Av1BlockSize.Block16x4]],
[[Av1BlockSize.Block16x64, Av1BlockSize.Block16x32], [Av1BlockSize.Invalid, Av1BlockSize.Block8x32]],
[[Av1BlockSize.Block64x16, Av1BlockSize.Invalid], [Av1BlockSize.Block32x16, Av1BlockSize.Block32x8]]
];
/// <summary>
/// Maps each block size to its largest permitted transform size.
/// </summary>
private static readonly Av1TransformSize[] MaxTransformSize = [
Av1TransformSize.Size4x4, Av1TransformSize.Size4x8, Av1TransformSize.Size8x4, Av1TransformSize.Size8x8,
Av1TransformSize.Size8x16, Av1TransformSize.Size16x8, Av1TransformSize.Size16x16, Av1TransformSize.Size16x32,
Av1TransformSize.Size32x16, Av1TransformSize.Size32x32, Av1TransformSize.Size32x64, Av1TransformSize.Size64x32,
Av1TransformSize.Size64x64, Av1TransformSize.Size64x64, Av1TransformSize.Size64x64, Av1TransformSize.Size64x64,
Av1TransformSize.Size4x16, Av1TransformSize.Size16x4, Av1TransformSize.Size8x32, Av1TransformSize.Size32x8,
Av1TransformSize.Size16x64, Av1TransformSize.Size64x16
];
/// <summary>
/// Contains the base-two logarithm of the sample count for each block size.
/// </summary>
private static readonly int[] PelsLog2Count =
[4, 5, 5, 6, 7, 7, 8, 9, 9, 10, 11, 11, 12, 13, 13, 14, 6, 6, 8, 8, 10, 10];
/// <summary>
/// Maps geometry dimension logarithms to an AV1 block size using the mode-decision scan's transposed axis convention.
/// </summary>
private static readonly Av1BlockSize[][] HeightWidthToSize = [
[Av1BlockSize.Block4x4, Av1BlockSize.Block4x8, Av1BlockSize.Block4x16, Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid],
[Av1BlockSize.Block8x4, Av1BlockSize.Block8x8, Av1BlockSize.Block8x16, Av1BlockSize.Block8x32, Av1BlockSize.Invalid, Av1BlockSize.Invalid],
[Av1BlockSize.Block16x4, Av1BlockSize.Block16x8, Av1BlockSize.Block16x16, Av1BlockSize.Block16x32, Av1BlockSize.Block16x64, Av1BlockSize.Invalid],
[Av1BlockSize.Invalid, Av1BlockSize.Block32x8, Av1BlockSize.Block32x16, Av1BlockSize.Block32x32, Av1BlockSize.Block32x64, Av1BlockSize.Invalid],
[Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x16, Av1BlockSize.Block64x32, Av1BlockSize.Block64x64, Av1BlockSize.Block64x128],
[Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block128x64, Av1BlockSize.Block128x128]
];
/// <summary>
/// Gets the block width in units of four samples.
/// </summary>
/// <param name="blockSize">The block size.</param>
/// <returns>The number of four-sample columns.</returns>
public static int Get4x4WideCount(this Av1BlockSize blockSize) => SizeWide[(int)blockSize];
/// <summary>
/// Gets the block height in units of four samples.
/// </summary>
/// <param name="blockSize">The block size.</param>
/// <returns>The number of four-sample rows.</returns>
public static int Get4x4HighCount(this Av1BlockSize blockSize) => SizeHigh[(int)blockSize];
/// <summary>
/// Gets the block size from mode-decision geometry dimension logarithms, where zero represents four samples.
/// </summary>
/// <param name="widthLog2">The base-two width logarithm minus two.</param>
/// <param name="heightLog2">The base-two height logarithm minus two.</param>
/// <returns>The matching block size, or <see cref="Av1BlockSize.Invalid"/> for unsupported dimensions.</returns>
public static Av1BlockSize FromWidthAndHeight(uint widthLog2, uint heightLog2)
{
// Mode-decision geometry is ported with its source axis order, so its size lookup is indexed height first.
return HeightWidthToSize[heightLog2][widthLog2];
}
/// <summary>
/// Gets the block width in samples.
/// </summary>
/// <param name="blockSize">The block size.</param>
/// <returns>The block width in samples.</returns>
public static int GetWidth(this Av1BlockSize blockSize)
=> Get4x4WideCount(blockSize) << 2;
/// <summary>
/// Gets the block height in samples.
/// </summary>
/// <param name="blockSize">The block size.</param>
/// <returns>The block height in samples.</returns>
public static int GetHeight(this Av1BlockSize blockSize)
=> Get4x4HighCount(blockSize) << 2;
/// <summary>
/// Gets the base-two logarithm of the block width in units of four samples.
/// </summary>
/// <param name="blockSize">The block size.</param>
/// <returns>The base-two logarithm of the four-sample column count.</returns>
public static int Get4x4WidthLog2(this Av1BlockSize blockSize)
=> Av1Math.Log2(Get4x4WideCount(blockSize));
/// <summary>
/// Gets the base-two logarithm of the block height in units of four samples.
/// </summary>
/// <param name="blockSize">The block size.</param>
/// <returns>The base-two logarithm of the four-sample row count.</returns>
public static int Get4x4HeightLog2(this Av1BlockSize blockSize)
=> Av1Math.Log2(Get4x4HighCount(blockSize));
/// <summary>
/// Gets the entropy context group associated with the block size.
/// </summary>
/// <param name="blockSize">The block size.</param>
/// <returns>The zero-based size group in the inclusive range zero through three.</returns>
public static int GetSizeGroup(this Av1BlockSize blockSize)
{
// AV1 section 9.3 groups a block by its smaller dimension in 4x4 units and caps that logarithm at three.
// Deriving the value from the existing geometry tables exactly matches the reference decoder's size_group_lookup table.
return Math.Min(3, Math.Min(blockSize.Get4x4WidthLog2(), blockSize.Get4x4HeightLog2()));
}
/// <summary>
/// Gets the residual-plane block size for Boolean chroma subsampling flags.
/// </summary>
/// <param name="blockSize">The luma block size.</param>
/// <param name="subX">Indicates horizontal chroma subsampling.</param>
/// <param name="subY">Indicates vertical chroma subsampling.</param>
/// <returns>The corresponding residual-plane block size.</returns>
public static Av1BlockSize GetSubsampled(this Av1BlockSize blockSize, bool subX, bool subY)
=> GetSubsampled(blockSize, subX ? 1 : 0, subY ? 1 : 0);
/// <summary>
/// Gets the residual-plane block size for chroma subsampling shifts.
/// </summary>
/// <param name="blockSize">The luma block size.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <returns>The corresponding residual-plane block size, or <see cref="Av1BlockSize.Invalid"/> when unavailable.</returns>
public static Av1BlockSize GetSubsampled(this Av1BlockSize blockSize, int subX, int subY)
{
if (blockSize == Av1BlockSize.Invalid)
{
return Av1BlockSize.Invalid;
}
return SubSampled[(int)blockSize][subX][subY];
}
/// <summary>
/// Gets the maximum chroma transform size after applying plane subsampling and AV1 chroma transform limits.
/// </summary>
/// <param name="blockSize">The luma block size.</param>
/// <param name="subX">Indicates horizontal chroma subsampling.</param>
/// <param name="subY">Indicates vertical chroma subsampling.</param>
/// <returns>The maximum chroma transform size, or <see cref="Av1TransformSize.Invalid"/> when the plane block size is invalid.</returns>
public static Av1TransformSize GetMaxUvTransformSize(this Av1BlockSize blockSize, bool subX, bool subY)
{
Av1BlockSize planeBlockSize = blockSize.GetSubsampled(subX, subY);
Av1TransformSize uvTransformSize = Av1TransformSize.Invalid;
if (planeBlockSize < Av1BlockSize.AllSizes)
{
uvTransformSize = planeBlockSize.GetMaximumTransformSize();
}
return uvTransformSize switch
{
Av1TransformSize.Size64x64 or Av1TransformSize.Size64x32 or Av1TransformSize.Size32x64 => Av1TransformSize.Size32x32,
Av1TransformSize.Size64x16 => Av1TransformSize.Size32x16,
Av1TransformSize.Size16x64 => Av1TransformSize.Size16x32,
_ => uvTransformSize,
};
}
/// <summary>
/// Gets the largest square or rectangular transform size permitted for a block.
/// </summary>
/// <param name="blockSize">The block size.</param>
/// <returns>The maximum transform size.</returns>
public static Av1TransformSize GetMaximumTransformSize(this Av1BlockSize blockSize)
=> MaxTransformSize[(int)blockSize];
/// <summary>
/// Gets the base-two logarithm of the block's sample count.
/// </summary>
/// <param name="blockSize">The block size.</param>
/// <returns>The base-two logarithm of width multiplied by height.</returns>
public static int GetPelsLog2Count(this Av1BlockSize blockSize)
=> PelsLog2Count[(int)blockSize];
}

775
src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs

@ -0,0 +1,775 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using SixLabors.ImageSharp.ColorProfiles;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Contains the image-description fields stored in an AV1 codec-configuration item property.
/// </summary>
internal sealed class Av1CodecConfiguration
{
/// <summary>
/// The optional sequence-header payload retained from the configuration open bitstream units.
/// </summary>
private readonly byte[] configSequenceHeader;
/// <summary>
/// The sequence-header OBU extension byte, or <c>-1</c> when its header has no extension.
/// </summary>
private readonly int configSequenceHeaderExtension;
/// <summary>
/// The content light-level metadata carried by the configuration OBUs, or <see langword="null"/> when absent.
/// </summary>
private readonly HeifContentLightLevel? configContentLightLevel;
/// <summary>
/// The mastering-display color volume carried by the configuration OBUs, or <see langword="null"/> when absent.
/// </summary>
private readonly HeifMasteringDisplayColorVolume? configMasteringDisplayColorVolume;
/// <summary>
/// Initializes a new instance of the <see cref="Av1CodecConfiguration"/> class from an AV1 codec-configuration
/// item-property payload.
/// </summary>
/// <param name="boxBuffer">The configuration payload beginning with the marker and version fields.</param>
/// <param name="options">The general options governing metadata validation.</param>
public Av1CodecConfiguration(Span<byte> boxBuffer, DecoderOptions options)
{
if (boxBuffer.Length < 4)
{
throw new InvalidImageContentException("The AV1 codec configuration is truncated.");
}
Av1BitStreamReader reader = new(boxBuffer);
uint marker = reader.ReadLiteral(1);
uint version = reader.ReadLiteral(7);
if (marker != 1 || version != 1)
{
throw new InvalidImageContentException("The AV1 codec configuration has an invalid marker or version.");
}
this.SequenceProfile = (byte)reader.ReadLiteral(3);
this.SequenceLevelIndex = (byte)reader.ReadLiteral(5);
this.SequenceTier = reader.ReadLiteral(1) == 1;
this.HighBitDepth = reader.ReadLiteral(1) == 1;
this.TwelveBit = reader.ReadLiteral(1) == 1;
this.IsMonochrome = reader.ReadLiteral(1) == 1;
this.ChromaSubsamplingX = reader.ReadLiteral(1) == 1;
this.ChromaSubsamplingY = reader.ReadLiteral(1) == 1;
this.ChromaSamplePosition = (byte)reader.ReadLiteral(2);
if (this.SequenceProfile > (byte)ObuSequenceProfile.Professional
|| (this.TwelveBit && !this.HighBitDepth)
|| this.ChromaSamplePosition == (byte)ObuChromoSamplePosition.Reserved)
{
throw new InvalidImageContentException("The AV1 codec configuration contains invalid image-description fields.");
}
if (reader.ReadLiteral(3) != 0)
{
throw new InvalidImageContentException("The AV1 codec configuration has nonzero reserved bits.");
}
bool hasInitialPresentationDelay = reader.ReadLiteral(1) == 1;
uint delayOrReserved = reader.ReadLiteral(4);
if (!hasInitialPresentationDelay && delayOrReserved != 0)
{
throw new InvalidImageContentException("The AV1 codec configuration has a nonzero reserved delay field.");
}
// The delay syntax is consumed to validate the fixed record, but it describes sample presentation and has
// no meaning for the independently presented image item supported by this bounded container implementation.
ReadOnlySpan<byte> configObus = boxBuffer[4..];
int sequenceHeaderCount = ScanObus(
configObus,
true,
true,
"AV1 codec configuration",
options,
out int configSequenceHeaderOffset,
out int configSequenceHeaderLength,
out this.configSequenceHeaderExtension,
out this.configContentLightLevel,
out this.configMasteringDisplayColorVolume);
if (sequenceHeaderCount > 1)
{
throw new InvalidImageContentException("The AV1 codec configuration contains more than one sequence header OBU.");
}
if (configSequenceHeaderOffset < 0)
{
this.configSequenceHeader = [];
}
else
{
// The property-reader span is pooled and reused. Retain only the sequence-header bytes required for
// item/sample equivalence instead of materializing every optional configuration OBU.
this.configSequenceHeader = GC.AllocateUninitializedArray<byte>(configSequenceHeaderLength);
configObus.Slice(configSequenceHeaderOffset, configSequenceHeaderLength).CopyTo(this.configSequenceHeader);
}
}
/// <summary>
/// Gets the sequence profile declared for the coded image.
/// </summary>
public byte SequenceProfile { get; }
/// <summary>
/// Gets the first operating point's sequence-level index.
/// </summary>
public byte SequenceLevelIndex { get; }
/// <summary>
/// Gets a value indicating whether the first operating point uses the high tier.
/// </summary>
public bool SequenceTier { get; }
/// <summary>
/// Gets a value indicating whether the coded image uses more than eight bits per sample.
/// </summary>
public bool HighBitDepth { get; }
/// <summary>
/// Gets a value indicating whether the coded image uses twelve bits per sample.
/// </summary>
public bool TwelveBit { get; }
/// <summary>
/// Gets the coded image sample precision in bits.
/// </summary>
public HeifBitDepth BitDepth => this.TwelveBit ? HeifBitDepth.Bit12 : this.HighBitDepth ? HeifBitDepth.Bit10 : HeifBitDepth.Bit8;
/// <summary>
/// Gets a value indicating whether the coded image contains only a luma plane.
/// </summary>
public bool IsMonochrome { get; }
/// <summary>
/// Gets a value indicating whether the coded image's chroma planes are horizontally subsampled.
/// </summary>
public bool ChromaSubsamplingX { get; }
/// <summary>
/// Gets a value indicating whether the coded image's chroma planes are vertically subsampled.
/// </summary>
public bool ChromaSubsamplingY { get; }
/// <summary>
/// Gets the position of vertically subsampled chroma samples relative to luma samples.
/// </summary>
public byte ChromaSamplePosition { get; }
/// <summary>
/// Validates the AV1 image item OBU layout and metadata against its item properties and configuration record.
/// </summary>
/// <param name="itemData">The complete AV1 image item payload.</param>
/// <param name="itemContentLightLevel">
/// The content light-level property associated with the image item, or <see langword="null"/> when absent.
/// </param>
/// <param name="itemMasteringDisplayColorVolume">
/// The mastering-display property associated with the image item, or <see langword="null"/> when absent.
/// </param>
/// <param name="options">The general options governing metadata validation.</param>
/// <param name="contentLightLevel">
/// Receives the content light-level metadata carried by the combined configuration and item OBUs.
/// </param>
/// <param name="masteringDisplayColorVolume">
/// Receives the mastering-display metadata carried by the combined configuration and item OBUs.
/// </param>
public void ValidateItemData(
ReadOnlySpan<byte> itemData,
HeifContentLightLevel? itemContentLightLevel,
HeifMasteringDisplayColorVolume? itemMasteringDisplayColorVolume,
DecoderOptions options,
out HeifContentLightLevel? contentLightLevel,
out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume)
=> this.ValidateData(
itemData,
true,
"AV1 image item",
itemContentLightLevel,
itemMasteringDisplayColorVolume,
options,
out contentLightLevel,
out masteringDisplayColorVolume);
/// <summary>
/// Validates one AV1 track sample against its sync-sample declaration, sample-entry metadata, and configuration record.
/// </summary>
/// <param name="sampleData">The complete AV1 sample payload.</param>
/// <param name="isSyncSample">Indicates that the sample is declared as a random-access point.</param>
/// <param name="sampleContentLightLevel">
/// The content light-level property associated with the sample entry, or <see langword="null"/> when absent.
/// </param>
/// <param name="sampleMasteringDisplayColorVolume">
/// The mastering-display property associated with the sample entry, or <see langword="null"/> when absent.
/// </param>
/// <param name="options">The general options governing metadata validation.</param>
/// <param name="contentLightLevel">
/// Receives the content light-level metadata carried by the combined configuration and sample OBUs.
/// </param>
/// <param name="masteringDisplayColorVolume">
/// Receives the mastering-display metadata carried by the combined configuration and sample OBUs.
/// </param>
public void ValidateSampleData(
ReadOnlySpan<byte> sampleData,
bool isSyncSample,
HeifContentLightLevel? sampleContentLightLevel,
HeifMasteringDisplayColorVolume? sampleMasteringDisplayColorVolume,
DecoderOptions options,
out HeifContentLightLevel? contentLightLevel,
out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume)
=> this.ValidateData(
sampleData,
isSyncSample,
"AV1 track sample",
sampleContentLightLevel,
sampleMasteringDisplayColorVolume,
options,
out contentLightLevel,
out masteringDisplayColorVolume);
/// <summary>
/// Validates one bounded AV1 payload while applying the item or track sequence-header requirement.
/// </summary>
/// <param name="data">The complete bounded AV1 payload.</param>
/// <param name="sequenceHeaderRequired">Indicates that exactly one sequence header is required.</param>
/// <param name="sourceName">The source description used by invalid-content errors.</param>
/// <param name="containerContentLightLevel">The content light-level property associated with the payload.</param>
/// <param name="containerMasteringDisplayColorVolume">The mastering-display property associated with the payload.</param>
/// <param name="options">The general options governing metadata validation.</param>
/// <param name="contentLightLevel">Receives validated OBU content light-level metadata.</param>
/// <param name="masteringDisplayColorVolume">Receives validated OBU mastering-display metadata.</param>
private void ValidateData(
ReadOnlySpan<byte> data,
bool sequenceHeaderRequired,
string sourceName,
HeifContentLightLevel? containerContentLightLevel,
HeifMasteringDisplayColorVolume? containerMasteringDisplayColorVolume,
DecoderOptions options,
out HeifContentLightLevel? contentLightLevel,
out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume)
{
int sequenceHeaderCount = ScanObus(
data,
false,
false,
sourceName,
options,
out int dataSequenceHeaderOffset,
out int dataSequenceHeaderLength,
out int dataSequenceHeaderExtension,
out HeifContentLightLevel? dataObuContentLightLevel,
out HeifMasteringDisplayColorVolume? dataObuMasteringDisplayColorVolume);
if (sequenceHeaderCount > 1 || (sequenceHeaderRequired && sequenceHeaderCount != 1))
{
string requirement = sequenceHeaderRequired ? "exactly one" : "at most one";
throw new InvalidImageContentException($"The {sourceName} contains {sequenceHeaderCount} sequence header OBUs instead of {requirement}.");
}
if (this.configSequenceHeader.Length != 0 && dataSequenceHeaderOffset >= 0)
{
ReadOnlySpan<byte> dataSequenceHeader = data.Slice(
dataSequenceHeaderOffset,
dataSequenceHeaderLength);
// Compare the extension and payload rather than the encoded OBU size. Configuration OBUs must carry a
// size field while a payload's final OBU may omit one, and different legal LEB128 widths do not alter
// the Sequence Header OBU being repeated.
if (this.configSequenceHeaderExtension != dataSequenceHeaderExtension
|| !this.configSequenceHeader.AsSpan().SequenceEqual(dataSequenceHeader))
{
throw new InvalidImageContentException(
$"The AV1 codec configuration sequence header does not match the {sourceName} sequence header.");
}
}
contentLightLevel = null;
masteringDisplayColorVolume = null;
if (options.SkipMetadata)
{
return;
}
try
{
ValidateContentLightLevel(this.configContentLightLevel, containerContentLightLevel, "AV1 codec configuration");
ValidateContentLightLevel(dataObuContentLightLevel, containerContentLightLevel, sourceName);
ValidateMasteringDisplayColorVolume(
this.configMasteringDisplayColorVolume,
containerMasteringDisplayColorVolume,
"AV1 codec configuration");
ValidateMasteringDisplayColorVolume(
dataObuMasteringDisplayColorVolume,
containerMasteringDisplayColorVolume,
sourceName);
if (this.configContentLightLevel is not null
&& dataObuContentLightLevel is not null
&& !ContentLightLevelsMatch(this.configContentLightLevel.Value, dataObuContentLightLevel.Value))
{
throw new InvalidImageContentException(
$"The AV1 codec configuration and {sourceName} contain conflicting content light-level metadata.");
}
if (this.configMasteringDisplayColorVolume is not null
&& dataObuMasteringDisplayColorVolume is not null
&& this.configMasteringDisplayColorVolume.Value != dataObuMasteringDisplayColorVolume.Value)
{
throw new InvalidImageContentException(
$"The AV1 codec configuration and {sourceName} contain conflicting mastering-display metadata.");
}
// Configuration OBUs precede the payload OBUs, so a payload OBU supplies the effective value when both
// sequences repeat the same metadata type.
contentLightLevel = dataObuContentLightLevel ?? this.configContentLightLevel;
masteringDisplayColorVolume = dataObuMasteringDisplayColorVolume ?? this.configMasteringDisplayColorVolume;
}
catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(options, ex))
{
// Conflicting optional OBU metadata is discarded without weakening OBU framing or sequence-header checks.
}
}
/// <summary>
/// Determines whether another item configuration describes the same coded-image sample layout.
/// </summary>
/// <param name="other">The configuration to compare.</param>
/// <returns><see langword="true"/> when every fixed image-description field is equal.</returns>
public bool HasMatchingImageConfiguration(Av1CodecConfiguration other)
=> this.SequenceProfile == other.SequenceProfile
&& this.SequenceLevelIndex == other.SequenceLevelIndex
&& this.SequenceTier == other.SequenceTier
&& this.HighBitDepth == other.HighBitDepth
&& this.TwelveBit == other.TwelveBit
&& this.IsMonochrome == other.IsMonochrome
&& this.ChromaSubsamplingX == other.ChromaSubsamplingX
&& this.ChromaSubsamplingY == other.ChromaSubsamplingY
&& this.ChromaSamplePosition == other.ChromaSamplePosition;
/// <summary>
/// Validates the configuration fields against the sequence header that describes the coded image item.
/// </summary>
/// <param name="sequenceHeader">The decoded AV1 sequence header.</param>
public void Validate(ObuSequenceHeader sequenceHeader)
{
ObuOperatingPoint operatingPoint = sequenceHeader.OperatingPoint[0];
ObuColorConfig colorConfig = sequenceHeader.ColorConfig;
bool highBitDepth = colorConfig.BitDepth is Av1BitDepth.TenBit or Av1BitDepth.TwelveBit;
bool twelveBit = colorConfig.BitDepth == Av1BitDepth.TwelveBit;
if (this.SequenceProfile != (byte)sequenceHeader.SequenceProfile
|| this.SequenceLevelIndex != operatingPoint.SequenceLevelIndex
|| this.SequenceTier != (operatingPoint.SequenceTier != 0)
|| this.HighBitDepth != highBitDepth
|| this.TwelveBit != twelveBit
|| this.IsMonochrome != colorConfig.IsMonochrome
|| this.ChromaSubsamplingX != colorConfig.SubSamplingX
|| this.ChromaSubsamplingY != colorConfig.SubSamplingY
|| this.ChromaSamplePosition != (byte)colorConfig.ChromaSamplePosition)
{
throw new InvalidImageContentException("The AV1 item configuration does not match its sequence header.");
}
}
/// <summary>
/// Scans a low-overhead AV1 OBU sequence and locates its still-image description metadata.
/// </summary>
/// <param name="data">The complete bounded OBU sequence.</param>
/// <param name="requireSizeFields">Indicates that every OBU must carry its registered payload-size field.</param>
/// <param name="sequenceHeaderMustBeFirst">
/// Indicates that a sequence-header OBU, when present, must be the first OBU in the sequence.
/// </param>
/// <param name="sourceName">The source description used by invalid-content errors.</param>
/// <param name="options">The general options governing metadata validation.</param>
/// <param name="sequenceHeaderOffset">Receives the first sequence-header payload offset, or <c>-1</c>.</param>
/// <param name="sequenceHeaderLength">Receives the first sequence-header payload length.</param>
/// <param name="sequenceHeaderExtension">Receives the first sequence-header extension byte, or <c>-1</c>.</param>
/// <param name="contentLightLevel">
/// Receives the content light-level metadata carried by the sequence, or <see langword="null"/> when absent.
/// </param>
/// <param name="masteringDisplayColorVolume">
/// Receives the mastering-display metadata carried by the sequence, or <see langword="null"/> when absent.
/// </param>
/// <returns>The number of sequence-header OBUs in the sequence.</returns>
private static int ScanObus(
ReadOnlySpan<byte> data,
bool requireSizeFields,
bool sequenceHeaderMustBeFirst,
string sourceName,
DecoderOptions options,
out int sequenceHeaderOffset,
out int sequenceHeaderLength,
out int sequenceHeaderExtension,
out HeifContentLightLevel? contentLightLevel,
out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume)
{
sequenceHeaderOffset = -1;
sequenceHeaderLength = 0;
sequenceHeaderExtension = -1;
contentLightLevel = null;
masteringDisplayColorVolume = null;
int sequenceHeaderCount = 0;
int obuIndex = 0;
int offset = 0;
while (offset < data.Length)
{
byte header = data[offset++];
if ((header & 0x80) != 0)
{
throw new InvalidImageContentException($"The {sourceName} contains an OBU with a set forbidden header bit.");
}
// The reference decoder deliberately ignores obu_reserved_1bit. The bit does not alter the OBU boundary or
// decoded syntax, so the bounded container scan must not reject data that the production parser accepts.
ObuType type = (ObuType)((header >> 3) & 0x0F);
bool hasExtension = (header & 0x04) != 0;
bool hasSizeField = (header & 0x02) != 0;
int extension = -1;
if (hasExtension)
{
if (offset >= data.Length)
{
throw new InvalidImageContentException($"The {sourceName} contains a truncated OBU extension header.");
}
// extension_header_reserved_3bits is also consumed but ignored by the reference decoder.
extension = data[offset++];
}
if (requireSizeFields && !hasSizeField)
{
throw new InvalidImageContentException($"The {sourceName} contains an OBU without its required payload-size field.");
}
int payloadLength;
if (hasSizeField)
{
payloadLength = ReadObuPayloadLength(data, ref offset, sourceName);
}
else
{
// Low-overhead image item syntax permits only the final OBU to omit its size, in which case the
// remaining item bytes are that OBU's payload and cannot contain another independently parsed OBU.
payloadLength = data.Length - offset;
}
if (payloadLength > data.Length - offset)
{
throw new InvalidImageContentException($"The {sourceName} contains an OBU payload that exceeds its data boundary.");
}
if (type == ObuType.SequenceHeader)
{
if (sequenceHeaderMustBeFirst && obuIndex != 0)
{
throw new InvalidImageContentException($"The {sourceName} contains a sequence header OBU after another OBU.");
}
sequenceHeaderCount++;
if (sequenceHeaderOffset < 0)
{
sequenceHeaderOffset = offset;
sequenceHeaderLength = payloadLength;
sequenceHeaderExtension = extension;
}
}
else if (type == ObuType.Metadata && !options.SkipMetadata)
{
try
{
ReadHdrMetadata(
data.Slice(offset, payloadLength),
sourceName,
out HeifContentLightLevel? obuContentLightLevel,
out HeifMasteringDisplayColorVolume? obuMasteringDisplayColorVolume);
if (obuContentLightLevel is not null)
{
if (contentLightLevel is not null
&& !ContentLightLevelsMatch(contentLightLevel.Value, obuContentLightLevel.Value))
{
throw new InvalidImageContentException($"The {sourceName} contains conflicting content light-level metadata OBUs.");
}
contentLightLevel = obuContentLightLevel;
}
if (obuMasteringDisplayColorVolume is not null)
{
if (masteringDisplayColorVolume is not null
&& masteringDisplayColorVolume.Value != obuMasteringDisplayColorVolume.Value)
{
throw new InvalidImageContentException($"The {sourceName} contains conflicting mastering-display metadata OBUs.");
}
masteringDisplayColorVolume = obuMasteringDisplayColorVolume;
}
}
catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(options, ex))
{
// The OBU payload remains bounded by the image-data scan; only its invalid optional metadata is discarded.
}
}
offset += payloadLength;
obuIndex++;
}
return sequenceHeaderCount;
}
/// <summary>
/// Reads a bounded AV1 little-endian base-128 OBU payload length.
/// </summary>
/// <param name="data">The complete bounded OBU sequence.</param>
/// <param name="offset">The current byte offset, advanced past the encoded length.</param>
/// <param name="sourceName">The source description used by invalid-content errors.</param>
/// <returns>The payload length representable by the current item buffer.</returns>
private static int ReadObuPayloadLength(ReadOnlySpan<byte> data, ref int offset, string sourceName)
{
ulong value = ReadLeb128(data, ref offset, sourceName, "OBU payload length");
if (value > int.MaxValue)
{
throw new InvalidImageContentException($"The {sourceName} contains an OBU payload too large to buffer.");
}
return (int)value;
}
/// <summary>
/// Reads still-image high-dynamic-range data from an AV1 metadata OBU payload.
/// </summary>
/// <param name="payload">The bounded metadata OBU payload.</param>
/// <param name="sourceName">The source description used by invalid-content errors.</param>
/// <param name="contentLightLevel">Receives decoded content light-level metadata when present.</param>
/// <param name="masteringDisplayColorVolume">Receives decoded mastering-display metadata when present.</param>
public static void ReadHdrMetadata(
ReadOnlySpan<byte> payload,
string sourceName,
out HeifContentLightLevel? contentLightLevel,
out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume)
{
contentLightLevel = null;
masteringDisplayColorVolume = null;
int offset = 0;
ulong metadataType = ReadLeb128(payload, ref offset, sourceName, "metadata type");
if (metadataType != (ulong)ObuMetadataType.HdrCll
&& metadataType != (ulong)ObuMetadataType.HdrMdcv)
{
return;
}
int metadataLength = metadataType == (ulong)ObuMetadataType.HdrCll ? 4 : 24;
if (payload.Length - offset <= metadataLength)
{
throw new InvalidImageContentException($"The {sourceName} contains truncated HDR metadata or no trailing bits.");
}
ReadOnlySpan<byte> metadataData = payload.Slice(offset, metadataLength);
ValidateByteAlignedMetadataTrailingBits(payload[(offset + metadataLength)..], sourceName);
if (metadataType == (ulong)ObuMetadataType.HdrCll)
{
contentLightLevel = new HeifContentLightLevel(
BinaryPrimitives.ReadUInt16BigEndian(metadataData),
BinaryPrimitives.ReadUInt16BigEndian(metadataData[2..]));
return;
}
const float chromaticityScale = 1F / 65536F;
const double maximumLuminanceScale = 1D / 256D;
const double minimumLuminanceScale = 1D / 16384D;
// AV1 stores the primaries in R, G, B order and uses codec-specific fixed-point units that differ from the
// ISOBMFF mdcv property. Decode both representations to the same observable ImageSharp color coordinates.
CieXyChromaticityCoordinates redPrimary = new(
BinaryPrimitives.ReadUInt16BigEndian(metadataData) * chromaticityScale,
BinaryPrimitives.ReadUInt16BigEndian(metadataData[2..]) * chromaticityScale);
CieXyChromaticityCoordinates greenPrimary = new(
BinaryPrimitives.ReadUInt16BigEndian(metadataData[4..]) * chromaticityScale,
BinaryPrimitives.ReadUInt16BigEndian(metadataData[6..]) * chromaticityScale);
CieXyChromaticityCoordinates bluePrimary = new(
BinaryPrimitives.ReadUInt16BigEndian(metadataData[8..]) * chromaticityScale,
BinaryPrimitives.ReadUInt16BigEndian(metadataData[10..]) * chromaticityScale);
masteringDisplayColorVolume = new HeifMasteringDisplayColorVolume(
new RgbPrimariesChromaticityCoordinates(redPrimary, greenPrimary, bluePrimary),
new CieXyChromaticityCoordinates(
BinaryPrimitives.ReadUInt16BigEndian(metadataData[12..]) * chromaticityScale,
BinaryPrimitives.ReadUInt16BigEndian(metadataData[14..]) * chromaticityScale),
BinaryPrimitives.ReadUInt32BigEndian(metadataData[16..]) * maximumLuminanceScale,
BinaryPrimitives.ReadUInt32BigEndian(metadataData[20..]) * minimumLuminanceScale);
}
/// <summary>
/// Validates the trailing bits of byte-aligned fixed-length AV1 metadata.
/// </summary>
/// <param name="trailingData">The metadata payload bytes following its fixed fields.</param>
/// <param name="sourceName">The source description used by invalid-content errors.</param>
private static void ValidateByteAlignedMetadataTrailingBits(ReadOnlySpan<byte> trailingData, string sourceName)
{
byte lastNonzeroByte = 0;
for (int i = trailingData.Length - 1; i >= 0; i--)
{
if (trailingData[i] != 0)
{
lastNonzeroByte = trailingData[i];
break;
}
}
// Both fixed HDR structures end on a byte boundary. Zero padding after the required 0x80 byte is accepted,
// so locate the last nonzero byte rather than assuming the OBU payload ends immediately after trailing_bits().
if (lastNonzeroByte != 0x80)
{
throw new InvalidImageContentException($"The {sourceName} HDR metadata has invalid trailing bits.");
}
}
/// <summary>
/// Reads a bounded AV1 little-endian base-128 value.
/// </summary>
/// <param name="data">The complete bounded byte sequence.</param>
/// <param name="offset">The current byte offset, advanced past the encoded value.</param>
/// <param name="sourceName">The source description used by invalid-content errors.</param>
/// <param name="valueName">The value description used by invalid-content errors.</param>
/// <returns>The decoded unsigned value.</returns>
private static ulong ReadLeb128(
ReadOnlySpan<byte> data,
ref int offset,
string sourceName,
string valueName)
{
ulong value = 0;
for (int byteIndex = 0; byteIndex < 8; byteIndex++)
{
if (offset >= data.Length)
{
throw new InvalidImageContentException($"The {sourceName} contains a truncated {valueName}.");
}
byte current = data[offset++];
value |= (ulong)(current & 0x7F) << (byteIndex * 7);
if ((current & 0x80) == 0)
{
if (value > uint.MaxValue)
{
throw new InvalidImageContentException($"The {sourceName} contains a {valueName} larger than the AV1 32-bit limit.");
}
return value;
}
}
throw new InvalidImageContentException($"The {sourceName} contains an unterminated {valueName}.");
}
/// <summary>
/// Validates content light-level metadata against the corresponding image-item property when both are present.
/// </summary>
/// <param name="obuContentLightLevel">The value carried by an AV1 metadata OBU.</param>
/// <param name="itemContentLightLevel">The value carried by the associated image-item property.</param>
/// <param name="sourceName">The OBU source description used by invalid-content errors.</param>
private static void ValidateContentLightLevel(
HeifContentLightLevel? obuContentLightLevel,
HeifContentLightLevel? itemContentLightLevel,
string sourceName)
{
if (obuContentLightLevel is not null
&& itemContentLightLevel is not null
&& !ContentLightLevelsMatch(obuContentLightLevel.Value, itemContentLightLevel.Value))
{
throw new InvalidImageContentException($"The {sourceName} content light-level metadata does not match the image-item property.");
}
}
/// <summary>
/// Determines whether two content light-level descriptions carry the same observable values.
/// </summary>
/// <param name="left">The first content light-level description.</param>
/// <param name="right">The second content light-level description.</param>
/// <returns><see langword="true"/> when both light-level fields are equal.</returns>
private static bool ContentLightLevelsMatch(HeifContentLightLevel left, HeifContentLightLevel right)
{
return left.MaximumContentLightLevel == right.MaximumContentLightLevel
&& left.MaximumPictureAverageLightLevel == right.MaximumPictureAverageLightLevel;
}
/// <summary>
/// Validates mastering-display metadata against the corresponding image-item property when both are present.
/// </summary>
/// <param name="obuColorVolume">The value carried by an AV1 metadata OBU.</param>
/// <param name="itemColorVolume">The value carried by the associated image-item property.</param>
/// <param name="sourceName">The OBU source description used by invalid-content errors.</param>
private static void ValidateMasteringDisplayColorVolume(
HeifMasteringDisplayColorVolume? obuColorVolume,
HeifMasteringDisplayColorVolume? itemColorVolume,
string sourceName)
{
if (obuColorVolume is not null
&& itemColorVolume is not null
&& !MasteringDisplayColorVolumesMatch(obuColorVolume.Value, itemColorVolume.Value))
{
throw new InvalidImageContentException($"The {sourceName} mastering-display metadata does not match the image-item property.");
}
}
/// <summary>
/// Determines whether AV1 and ISOBMFF mastering-display values agree within their fixed-point precision.
/// </summary>
/// <param name="obuColorVolume">The mastering-display values decoded from the AV1 representation.</param>
/// <param name="itemColorVolume">The mastering-display values decoded from the ISOBMFF representation.</param>
/// <returns><see langword="true"/> when all decoded values agree within their combined quantization error.</returns>
private static bool MasteringDisplayColorVolumesMatch(
HeifMasteringDisplayColorVolume obuColorVolume,
HeifMasteringDisplayColorVolume itemColorVolume)
{
const float chromaticityTolerance = ((1F / 65536F) + (1F / 50000F)) / 2F;
const double maximumLuminanceTolerance = ((1D / 256D) + (1D / 10000D)) / 2D;
const double minimumLuminanceTolerance = ((1D / 16384D) + (1D / 10000D)) / 2D;
return ChromaticitiesMatch(obuColorVolume.Primaries.R, itemColorVolume.Primaries.R, chromaticityTolerance)
&& ChromaticitiesMatch(obuColorVolume.Primaries.G, itemColorVolume.Primaries.G, chromaticityTolerance)
&& ChromaticitiesMatch(obuColorVolume.Primaries.B, itemColorVolume.Primaries.B, chromaticityTolerance)
&& ChromaticitiesMatch(obuColorVolume.WhitePoint, itemColorVolume.WhitePoint, chromaticityTolerance)
&& ValuesMatch(obuColorVolume.MaximumLuminance, itemColorVolume.MaximumLuminance, maximumLuminanceTolerance)
&& ValuesMatch(obuColorVolume.MinimumLuminance, itemColorVolume.MinimumLuminance, minimumLuminanceTolerance);
}
/// <summary>
/// Determines whether two chromaticity-coordinate pairs agree within the supplied fixed-point tolerance.
/// </summary>
/// <param name="left">The first chromaticity-coordinate pair.</param>
/// <param name="right">The second chromaticity-coordinate pair.</param>
/// <param name="tolerance">The maximum permitted difference on either coordinate axis.</param>
/// <returns><see langword="true"/> when both coordinate differences are within the tolerance.</returns>
private static bool ChromaticitiesMatch(
CieXyChromaticityCoordinates left,
CieXyChromaticityCoordinates right,
float tolerance)
{
return ValuesMatch(left.X, right.X, tolerance)
&& ValuesMatch(left.Y, right.Y, tolerance);
}
/// <summary>
/// Determines whether two decoded fixed-point values agree within the supplied tolerance.
/// </summary>
/// <param name="left">The first decoded value.</param>
/// <param name="right">The second decoded value.</param>
/// <param name="tolerance">The maximum permitted absolute difference.</param>
/// <returns><see langword="true"/> when the absolute difference does not exceed the tolerance.</returns>
private static bool ValuesMatch(double left, double right, double tolerance)
=> Math.Abs(left - right) <= tolerance;
}

30
src/ImageSharp/Formats/Heif/Av1/Av1ColorFormat.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Identifies the AV1 luma and chroma plane sampling layout.
/// </summary>
internal enum Av1ColorFormat
{
/// <summary>
/// Monochrome luma samples without chroma planes.
/// </summary>
Yuv400,
/// <summary>
/// Chroma samples subsampled by two horizontally and vertically.
/// </summary>
Yuv420,
/// <summary>
/// Chroma samples subsampled by two horizontally.
/// </summary>
Yuv422,
/// <summary>
/// Full-resolution luma and chroma samples.
/// </summary>
Yuv444,
}

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

@ -0,0 +1,318 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Defines shared AV1 syntax, geometry, entropy, and transform limits.
/// </summary>
internal static class Av1Constants
{
/// <summary>
/// The highest sequence profile defined by AV1.
/// </summary>
public const ObuSequenceProfile MaxSequenceProfile = ObuSequenceProfile.Professional;
/// <summary>
/// The number of bits used for an operating-point level index.
/// </summary>
public const int LevelBits = 5;
/// <summary>
/// The maximum number of operating points declared by one AV1 sequence header.
/// </summary>
public const int MaxOperatingPointCount = 32;
/// <summary>
/// The maximum number of spatial layers identified by an AV1 OBU extension header.
/// </summary>
public const int MaxSpatialLayerCount = 4;
/// <summary>
/// The number of bits used to signal a super-resolution denominator offset.
/// </summary>
public const int SuperResolutionScaleBits = 3;
/// <summary>
/// The fixed numerator of the AV1 super-resolution scaling ratio.
/// </summary>
public const int ScaleNumerator = 8;
/// <summary>
/// The number of reference frames that can be used for inter prediction.
/// </summary>
public const int ReferencesPerFrame = 7;
/// <summary>
/// The maximum area of a tile in units of luma samples.
/// </summary>
public const int MaxTileArea = 4096 * 2304;
/// <summary>
/// The maximum width of a tile in units of luma samples.
/// </summary>
public const int MaxTileWidth = 4096;
/// <summary>
/// The maximum number of tile columns.
/// </summary>
public const int MaxTileColumnCount = 64;
/// <summary>
/// The maximum number of tile rows.
/// </summary>
public const int MaxTileRowCount = 64;
/// <summary>
/// The number of frames that can be stored for future reference.
/// </summary>
public const int ReferenceFrameCount = 8;
/// <summary>
/// The primary-reference-frame value indicating that no primary reference is selected.
/// </summary>
public const uint PrimaryReferenceFrameNone = 7;
/// <summary>
/// The number of bits used to signal a primary reference frame.
/// </summary>
public const int PrimaryReferenceBits = 3;
/// <summary>
/// The number of segments allowed in a segmentation map.
/// </summary>
public const int MaxSegmentCount = 8;
/// <summary>
/// The smallest signaled denominator for an active super-resolution ratio.
/// </summary>
public const int SuperResolutionScaleDenominatorMinimum = 9;
/// <summary>
/// The base-two logarithm of the maximum superblock size in luma samples.
/// </summary>
public const int MaxSuperBlockSizeLog2 = 7;
/// <summary>
/// The base-two logarithm of the smallest mode-info block size in luma samples.
/// </summary>
public const int ModeInfoSizeLog2 = 2;
/// <summary>
/// The maximum quantizer index.
/// </summary>
public const int MaxQ = 255;
/// <summary>
/// The number of segmentation features.
/// </summary>
public const int SegmentationLevelMax = 8;
/// <summary>
/// The maximum loop-restoration tile size in samples.
/// </summary>
public const int RestorationMaxTileSize = 256;
/// <summary>
/// The number of independent Wiener filter coefficients per direction.
/// </summary>
public const int WienerCoefficientCount = 3;
/// <summary>
/// The number of luma and chroma frame loop-filter levels.
/// </summary>
public const int FrameLoopFilterCount = 4;
/// <summary>
/// The first quantizer-delta magnitude encoded through the escape path.
/// </summary>
public const int DeltaQuantizerSmall = 3;
/// <summary>
/// The first loop-filter-delta magnitude encoded through the escape path.
/// </summary>
public const int DeltaLoopFilterSmall = 3;
/// <summary>
/// The maximum loop-filter strength.
/// </summary>
public const int MaxLoopFilter = 63;
/// <summary>
/// The maximum directional-prediction angle-delta magnitude.
/// </summary>
public const int MaxAngleDelta = 3;
/// <summary>
/// The maximum number of color planes.
/// </summary>
public const int MaxPlanes = 3;
/// <summary>
/// The number of reference-frame types, including the intra type.
/// </summary>
public const int TotalReferencesPerFrame = 8;
/// <summary>
/// The maximum palette size.
/// </summary>
public const int PaletteMaxSize = 8;
/// <summary>
/// The number of transform-size probability categories.
/// </summary>
public const int MaxTransformCategories = 4;
/// <summary>
/// The number of cumulative coefficient-level magnitude contexts.
/// </summary>
public const int CoefficientContextCount = 6;
/// <summary>
/// The number of coefficient magnitudes represented by base symbols before base-range coding.
/// </summary>
public const int BaseLevelsCount = 2;
/// <summary>
/// The maximum coefficient magnitude increment represented by base-range symbols.
/// </summary>
public const int CoefficientBaseRange = 12;
/// <summary>
/// The maximum transform dimension in samples.
/// </summary>
public const int MaxTransformSize = 1 << 6;
/// <summary>
/// The maximum transform dimension in units of four samples.
/// </summary>
public const int MaxTransformSizeUnit = MaxTransformSize >> 2;
/// <summary>
/// The number of low-order bits reserved for a cumulative coefficient-level context.
/// </summary>
public const int CoefficientContextBitCount = 3;
/// <summary>
/// The mask selecting the cumulative coefficient-level magnitude bits.
/// </summary>
public const int CoefficientContextMask = (1 << CoefficientContextBitCount) - 1;
/// <summary>
/// The base-two logarithm of the horizontal coefficient-context padding.
/// </summary>
public const int TransformPadHorizontalLog2 = 2;
/// <summary>
/// The horizontal coefficient-context padding in elements.
/// </summary>
public const int TransformPadHorizontal = 1 << TransformPadHorizontalLog2;
/// <summary>
/// The total vertical coefficient-context padding in rows.
/// </summary>
public const int TransformPadVertical = 6;
/// <summary>
/// The trailing coefficient-context padding in elements.
/// </summary>
public const int TransformPadEnd = 16;
/// <summary>
/// The maximum padded two-dimensional coefficient-context allocation size.
/// </summary>
public const int TransformPad2d = ((MaxTransformSize + TransformPadHorizontal) * (MaxTransformSize + TransformPadVertical)) + TransformPadEnd;
/// <summary>
/// The coefficient-context padding above a transform.
/// </summary>
public const int TransformPadTop = 2;
/// <summary>
/// The coefficient-context padding below a transform.
/// </summary>
public const int TransformPadBottom = 4;
/// <summary>
/// The largest symbol in a coefficient base-range distribution.
/// </summary>
public const int BaseRangeSizeMinus1 = 3;
/// <summary>
/// The largest coefficient magnitude represented before Golomb coding.
/// </summary>
public const int MaxBaseRange = 15;
/// <summary>
/// The base-two logarithm of the chroma-from-luma alpha alphabet size.
/// </summary>
public const int ChromaFromLumaAlphabetSizeLog2 = 4;
/// <summary>
/// The number of quantization-matrix levels.
/// </summary>
public const int QuantificationMatrixLevelCount = 1 << 4;
/// <summary>
/// The fixed-point precision of each quantization-matrix element.
/// </summary>
public const int QuantizationMatrixElementBitCount = 5;
/// <summary>
/// The directional intra-prediction angle increment in degrees.
/// </summary>
public const int AngleStep = 3;
/// <summary>
/// The maximum number of stages in a one-dimensional transform function.
/// </summary>
public const int MaxTransformStageNumber = 12;
/// <summary>
/// The number of partition contexts per block-size logarithm.
/// </summary>
public const int PartitionProbabilitySet = 4;
/// <summary>
/// The number of square transform-size contexts that can signal extended transforms.
/// </summary>
public const int ExtendedTransformCount = 4;
/// <summary>
/// The highest variable-transform depth index.
/// </summary>
public const int MaxVarTransform = 2;
/// <summary>
/// The maximum number of transform blocks at one depth.
/// </summary>
public const int MaxTransformBlockCount = 16;
/// <summary>
/// Number of items in the <see cref="Av1PlaneType"/> enumeration.
/// </summary>
public const int PlaneTypeCount = 2;
/// <summary>
/// The maximum number of transform units stored for one encoded block.
/// </summary>
public const int MaxTransformUnitCount = 16;
/// <summary>
/// Gets the number of payload bits used by each segmentation feature.
/// </summary>
public static ReadOnlySpan<int> SegmentationFeatureBits => [8, 6, 6, 6, 6, 3, 0, 0];
/// <summary>
/// Gets values indicating whether each segmentation feature is signed.
/// </summary>
public static ReadOnlySpan<int> SegmentationFeatureSigned => [1, 1, 1, 1, 1, 0, 0, 0];
/// <summary>
/// Gets the maximum magnitude or value permitted for each segmentation feature.
/// </summary>
public static ReadOnlySpan<int> SegmentationFeatureMax => [MaxQ, MaxLoopFilter, MaxLoopFilter, MaxLoopFilter, MaxLoopFilter, 7, 0, 0];
}

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

@ -0,0 +1,996 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Color;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.FilmGrain;
using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.PixelFormats.Utils;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Decodes bounded AV1 image payloads and image-sequence samples into ImageSharp frames.
/// </summary>
internal sealed class Av1Decoder : IAv1TileReader, IDisposable
{
/// <summary>
/// The open-bitstream-unit parser for the current image item.
/// </summary>
private readonly ObuReader obuReader;
/// <summary>
/// The configuration used for decoded image and scratch-memory allocation.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// Reusable luma palette indices for the coding blocks in one superblock.
/// </summary>
private readonly Buffer2D<byte> lumaPaletteColorIndexMap;
/// <summary>
/// Reusable chroma palette indices for the coding blocks in one superblock.
/// </summary>
private readonly Buffer2D<byte> chromaPaletteColorIndexMap;
/// <summary>
/// The reconstructed references and selected presentation output owned by the current bounded decode session.
/// </summary>
private readonly Av1ReferenceFrameStore referenceFrames = new();
/// <summary>
/// The frame-base, tile-working, and published entropy contexts created for the first coded frame and then reused
/// for this bounded decoder session.
/// </summary>
private Av1FrameEntropyContexts? entropyContexts;
/// <summary>
/// The coded sequence governing the active reference map and reusable entropy session.
/// </summary>
private ObuSequenceHeader? entropySequenceHeader;
/// <summary>
/// The item codec configuration validated before reconstructing a completed frame.
/// </summary>
private Av1CodecConfiguration? codecConfiguration;
/// <summary>
/// The container color description applied before reconstructing a completed frame.
/// </summary>
private CicpProfile? containerColorProfile;
/// <summary>
/// The sequence header already validated for the current bounded payload.
/// </summary>
private ObuSequenceHeader? validatedSequenceHeader;
/// <summary>
/// The complete parser, sample buffer, and reconstruction state for the frame currently being decoded.
/// </summary>
private FrameDecodeState? frameDecodeState;
/// <summary>
/// Initializes a new instance of the <see cref="Av1Decoder"/> class.
/// </summary>
/// <param name="configuration">The configuration used for image and scratch-memory allocation.</param>
public Av1Decoder(Configuration configuration)
: this(configuration, 0)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Decoder"/> class for one selected AV1 operating point.
/// </summary>
/// <param name="configuration">The configuration used for image and scratch-memory allocation.</param>
/// <param name="operatingPointIndex">The zero-based sequence-header operating-point index to decode.</param>
public Av1Decoder(Configuration configuration, byte operatingPointIndex)
{
this.configuration = configuration;
this.obuReader = new(operatingPointIndex, this.referenceFrames);
Buffer2D<byte>? lumaPaletteColorIndexMap = null;
Buffer2D<byte>? chromaPaletteColorIndexMap = null;
try
{
// Sequential tile decoding needs only the palette indices belonging to the current superblock. Keeping
// two maximum-superblock surfaces at decoder scope makes the memory bound independent of frame size and
// reuses the same allocator rents across every frame in the bounded sequence.
int paletteMapLength = 1 << Av1Constants.MaxSuperBlockSizeLog2;
lumaPaletteColorIndexMap = configuration.MemoryAllocator.Allocate2D<byte>(paletteMapLength, paletteMapLength);
chromaPaletteColorIndexMap = configuration.MemoryAllocator.Allocate2D<byte>(paletteMapLength, paletteMapLength);
this.lumaPaletteColorIndexMap = lumaPaletteColorIndexMap;
this.chromaPaletteColorIndexMap = chromaPaletteColorIndexMap;
}
catch
{
chromaPaletteColorIndexMap?.Dispose();
lumaPaletteColorIndexMap?.Dispose();
throw;
}
}
/// <summary>
/// Gets the final retained shown-frame header, or <see langword="null"/> before a shown frame completes.
/// </summary>
public ObuFrameHeader? FrameHeader { get; private set; }
/// <summary>
/// Gets the sequence header governing the final retained shown frame, or <see langword="null"/> before one completes.
/// </summary>
public ObuSequenceHeader? SequenceHeader { get; private set; }
/// <summary>
/// Gets tile and superblock state for the most recently reconstructed frame, or <see langword="null"/> when no
/// frame was reconstructed or the output selected an existing reference without new tile syntax.
/// </summary>
public Av1FrameInfo? FrameInfo { get; private set; }
/// <summary>
/// Gets the inter-prediction features selected by every coded frame completed in the most recently decoded payload.
/// </summary>
public Av1InterPredictionFeatures DecodedInterPredictionFeatures { get; private set; }
/// <summary>
/// Gets the native planes of the current retained shown frame, or <see langword="null"/> before one completes.
/// </summary>
public Av1FrameBuffer<byte>? FrameBuffer => this.referenceFrames.OutputFrame?.FrameBuffer;
/// <summary>
/// Decodes a bounded AV1 image payload and presents its final shown frame.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <param name="buffer">The complete AV1 elementary-stream payload.</param>
/// <param name="containerColorProfile">
/// The container color description that supplies unspecified sequence-header color information.
/// </param>
/// <param name="codecConfiguration">
/// The item-associated AV1 codec configuration validated against the coded sequence header.
/// </param>
/// <param name="layeredImageIndex">The optional byte boundaries of a layered AV1 image item.</param>
/// <param name="presentationSize">The requested item presentation size, or an empty size for the coded dimensions.</param>
/// <returns>The decoded image.</returns>
public Image<TPixel> Decode<TPixel>(
Span<byte> buffer,
CicpProfile? containerColorProfile = null,
Av1CodecConfiguration? codecConfiguration = null,
Av1LayeredImageIndex? layeredImageIndex = null,
Size presentationSize = default)
where TPixel : unmanaged, IPixel<TPixel>
{
ImageFrame<TPixel> frame = this.DecodeFrame<TPixel>(
buffer,
containerColorProfile,
codecConfiguration,
out CicpProfile effectiveColorProfile,
layeredImageIndex,
presentationSize);
ImageMetadata metadata = new()
{
CicpProfile = effectiveColorProfile
};
HeifContentLightLevel? contentLightLevel = this.obuReader.ContentLightLevel;
HeifMasteringDisplayColorVolume? masteringDisplayColorVolume = this.obuReader.MasteringDisplayColorVolume;
if (contentLightLevel is not null || masteringDisplayColorVolume is not null)
{
HeifMetadata heifMetadata = metadata.GetHeifMetadata();
heifMetadata.ContentLightLevel = contentLightLevel;
heifMetadata.MasteringDisplayColorVolume = masteringDisplayColorVolume;
}
try
{
return new Image<TPixel>(this.configuration, metadata, [frame]);
}
catch
{
// Ownership transfers only after the image constructor accepts the decoded frame.
frame.Dispose();
throw;
}
}
/// <summary>
/// Decodes an AV1 elementary-stream payload into one independently owned ImageSharp frame.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <param name="buffer">The complete AV1 elementary-stream payload.</param>
/// <param name="containerColorProfile">
/// The container color description that supplies unspecified sequence-header color information.
/// </param>
/// <param name="codecConfiguration">
/// The AV1 codec configuration validated against the coded sequence header.
/// </param>
/// <param name="effectiveColorProfile">Receives the effective CICP description used for conversion.</param>
/// <param name="layeredImageIndex">The optional byte boundaries of a layered AV1 image item.</param>
/// <param name="presentationSize">The requested item presentation size, or an empty size for the coded dimensions.</param>
/// <returns>The decoded frame. Ownership transfers to the caller.</returns>
public ImageFrame<TPixel> DecodeFrame<TPixel>(
Span<byte> buffer,
CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration,
out CicpProfile effectiveColorProfile,
Av1LayeredImageIndex? layeredImageIndex = null,
Size presentationSize = default)
where TPixel : unmanaged, IPixel<TPixel>
{
using Av1FrameBuffer<byte> frameBuffer = this.DecodeFrameBuffer(
buffer,
containerColorProfile,
codecConfiguration,
out effectiveColorProfile,
out ObuFrameHeader frameHeader,
layeredImageIndex);
return this.ConvertToFrame<TPixel>(frameBuffer, frameHeader, effectiveColorProfile, presentationSize);
}
/// <summary>
/// Decodes the next visible sample in a bounded AV1 image sequence while retaining its reference state.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <param name="buffer">The complete AV1 sample payload.</param>
/// <param name="containerColorProfile">The container color description.</param>
/// <param name="codecConfiguration">The AV1 sample-entry configuration.</param>
/// <returns>The independently owned decoded frame.</returns>
public ImageFrame<TPixel> DecodeSequenceFrame<TPixel>(
Span<byte> buffer,
CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration)
where TPixel : unmanaged, IPixel<TPixel>
{
CicpProfile effectiveColorProfile = this.DecodePayload(
buffer,
containerColorProfile,
codecConfiguration,
null,
requireShownFrame: true);
Av1ReferenceFrame outputFrame = this.referenceFrames.ResolveOutput();
return this.ConvertToFrame<TPixel>(outputFrame.FrameBuffer, outputFrame.FrameHeader, effectiveColorProfile);
}
/// <summary>
/// Decodes one non-presented AV1 image-sequence sample while retaining its reference state.
/// </summary>
/// <param name="buffer">The complete AV1 sample payload.</param>
/// <param name="containerColorProfile">The container color description.</param>
/// <param name="codecConfiguration">The AV1 sample-entry configuration.</param>
public void DecodeSequenceReference(
Span<byte> buffer,
CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration)
=> _ = this.DecodePayload(
buffer,
containerColorProfile,
codecConfiguration,
null,
requireShownFrame: false);
/// <summary>
/// Decodes the next visible monochrome AV1 sequence sample and composes it into a color frame.
/// </summary>
/// <typeparam name="TPixel">The destination color pixel type.</typeparam>
/// <param name="buffer">The complete AV1 sample payload.</param>
/// <param name="containerColorProfile">The container color description.</param>
/// <param name="codecConfiguration">The AV1 sample-entry configuration.</param>
/// <param name="expectedCodedSize">The required coded dimensions.</param>
/// <param name="destination">The packed color frame receiving alpha values.</param>
/// <param name="outputSize">The complete presented size of the auxiliary image.</param>
/// <param name="destinationRectangle">The destination region receiving the alpha image.</param>
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
public void DecodeSequenceAlpha<TPixel>(
Span<byte> buffer,
CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration,
Size expectedCodedSize,
ImageFrame<TPixel> destination,
Size outputSize,
Rectangle destinationRectangle,
bool premultiplied)
where TPixel : unmanaged, IPixel<TPixel>
{
_ = this.DecodePayload(
buffer,
containerColorProfile,
codecConfiguration,
null,
requireShownFrame: true);
Av1ReferenceFrame outputFrame = this.referenceFrames.ResolveOutput();
this.ComposeAlpha(
outputFrame.FrameBuffer,
expectedCodedSize,
destination,
outputSize,
destinationRectangle,
premultiplied);
}
/// <summary>
/// Converts native AV1 planes into one independently owned packed-pixel frame.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <param name="frameBuffer">The decoded native planes.</param>
/// <param name="frameHeader">The completed header describing the decoded native planes.</param>
/// <param name="effectiveColorProfile">The effective CICP description.</param>
/// <param name="presentationSize">The requested item presentation size, or an empty size for the coded dimensions.</param>
/// <returns>The independently owned packed-pixel frame.</returns>
private ImageFrame<TPixel> ConvertToFrame<TPixel>(
Av1FrameBuffer<byte> frameBuffer,
ObuFrameHeader frameHeader,
CicpProfile effectiveColorProfile,
Size presentationSize = default)
where TPixel : unmanaged, IPixel<TPixel>
{
ImageFrame<TPixel>? resultFrame = null;
try
{
Size codedSize = new(
frameHeader.FrameSize.SuperResolutionUpscaledWidth,
frameHeader.FrameSize.FrameHeight);
// A selected lower spatial layer can only be scaled upward to the image item's ispe extent here.
// Other item-size corrections keep using the shared packed-pixel presentation path after decoding.
Size outputSize = presentationSize.Width >= codedSize.Width && presentationSize.Height >= codedSize.Height
? presentationSize
: codedSize;
resultFrame = new ImageFrame<TPixel>(
this.configuration,
outputSize.Width,
outputSize.Height);
Av1YuvConverter.ConvertToRgb(this.configuration, frameBuffer, resultFrame);
resultFrame.Metadata.CicpProfile = effectiveColorProfile.DeepClone();
return resultFrame;
}
catch
{
resultFrame?.Dispose();
throw;
}
}
/// <summary>
/// Decodes an AV1 elementary-stream payload and composes its luma plane directly into a packed color frame.
/// </summary>
/// <typeparam name="TPixel">The destination color pixel type.</typeparam>
/// <param name="buffer">The complete AV1 elementary-stream payload.</param>
/// <param name="containerColorProfile">
/// The container color description that supplies unspecified sequence-header color information.
/// </param>
/// <param name="codecConfiguration">The AV1 codec configuration validated against the coded sequence header.</param>
/// <param name="expectedCodedSize">The required coded dimensions, or an empty size when the item extent may differ.</param>
/// <param name="destination">The packed color frame receiving alpha values.</param>
/// <param name="outputSize">The complete presented size of the auxiliary image or grid tile.</param>
/// <param name="destinationRectangle">The destination region receiving the top-left portion of the presented alpha image.</param>
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
/// <param name="layeredImageIndex">The optional byte boundaries of a layered AV1 image item.</param>
public void DecodeAlpha<TPixel>(
Span<byte> buffer,
CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration,
Size expectedCodedSize,
ImageFrame<TPixel> destination,
Size outputSize,
Rectangle destinationRectangle,
bool premultiplied,
Av1LayeredImageIndex? layeredImageIndex = null)
where TPixel : unmanaged, IPixel<TPixel>
{
using Av1FrameBuffer<byte> frameBuffer = this.DecodeFrameBuffer(
buffer,
containerColorProfile,
codecConfiguration,
out _,
layeredImageIndex);
this.ComposeAlpha(
frameBuffer,
expectedCodedSize,
destination,
outputSize,
destinationRectangle,
premultiplied);
}
/// <summary>
/// Composes one decoded monochrome plane into a packed color frame.
/// </summary>
/// <typeparam name="TPixel">The destination color pixel type.</typeparam>
/// <param name="frameBuffer">The decoded monochrome planes.</param>
/// <param name="expectedCodedSize">The required coded dimensions.</param>
/// <param name="destination">The packed color frame receiving alpha values.</param>
/// <param name="outputSize">The complete presented size of the auxiliary image.</param>
/// <param name="destinationRectangle">The destination region receiving the alpha image.</param>
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
private void ComposeAlpha<TPixel>(
Av1FrameBuffer<byte> frameBuffer,
Size expectedCodedSize,
ImageFrame<TPixel> destination,
Size outputSize,
Rectangle destinationRectangle,
bool premultiplied)
where TPixel : unmanaged, IPixel<TPixel>
{
if (expectedCodedSize != default && (frameBuffer.Width != expectedCodedSize.Width || frameBuffer.Height != expectedCodedSize.Height))
{
throw new InvalidImageContentException("The decoded alpha sample dimensions do not match its visual sample entry.");
}
if (frameBuffer.ColorFormat != Av1ColorFormat.Yuv400)
{
// AVIF auxiliary alpha is the luma plane of an AV1 monochrome image. Accepting chroma-bearing payloads
// would silently reinterpret a color image and contradict the Sequence Header mono_chrome requirement.
throw new InvalidImageContentException("An AV1 auxiliary alpha image must be encoded as monochrome.");
}
Av1YuvConverter.ComposeAlpha(
this.configuration,
frameBuffer,
destination,
outputSize,
destinationRectangle,
premultiplied);
}
/// <summary>
/// Parses every coded frame in an AV1 payload and returns the final shown frame's native component planes.
/// </summary>
/// <param name="buffer">The complete AV1 elementary-stream payload.</param>
/// <param name="containerColorProfile">
/// The container color description that supplies unspecified sequence-header color information.
/// </param>
/// <param name="codecConfiguration">The AV1 codec configuration validated against the coded sequence header.</param>
/// <param name="effectiveColorProfile">Receives the effective CICP description associated with the native planes.</param>
/// <param name="layeredImageIndex">The optional byte boundaries of a layered AV1 image item.</param>
/// <returns>The reconstructed native frame buffer. Ownership transfers to the caller.</returns>
public Av1FrameBuffer<byte> DecodeFrameBuffer(
Span<byte> buffer,
CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration,
out CicpProfile effectiveColorProfile,
Av1LayeredImageIndex? layeredImageIndex = null)
=> this.DecodeFrameBuffer(
buffer,
containerColorProfile,
codecConfiguration,
out effectiveColorProfile,
out _,
layeredImageIndex);
/// <summary>
/// Parses every coded frame in an AV1 payload and returns the final shown frame's native planes and header.
/// </summary>
private Av1FrameBuffer<byte> DecodeFrameBuffer(
Span<byte> buffer,
CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration,
out CicpProfile effectiveColorProfile,
out ObuFrameHeader frameHeader,
Av1LayeredImageIndex? layeredImageIndex)
{
effectiveColorProfile = this.DecodePayload(
buffer,
containerColorProfile,
codecConfiguration,
layeredImageIndex,
requireShownFrame: true);
using Av1ReferenceFrame outputFrame = this.referenceFrames.TakeOutput();
frameHeader = outputFrame.FrameHeader;
return outputFrame.TakeFrameBuffer();
}
/// <summary>
/// Parses one bounded payload into the retained decoder session.
/// </summary>
/// <param name="buffer">The complete AV1 payload.</param>
/// <param name="containerColorProfile">The container color description.</param>
/// <param name="codecConfiguration">The AV1 codec configuration.</param>
/// <param name="layeredImageIndex">The optional layer byte boundaries.</param>
/// <param name="requireShownFrame">Whether the payload must produce a shown frame.</param>
/// <returns>The effective CICP description.</returns>
private CicpProfile DecodePayload(
Span<byte> buffer,
CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration,
Av1LayeredImageIndex? layeredImageIndex,
bool requireShownFrame)
{
this.codecConfiguration = codecConfiguration;
this.containerColorProfile = containerColorProfile;
this.validatedSequenceHeader = null;
this.obuReader.ResetMetadata();
this.SequenceHeader = null;
this.FrameHeader = null;
this.DecodedInterPredictionFeatures = Av1InterPredictionFeatures.None;
// Full tile syntax describes only frames reconstructed by this payload. Reference slots already own the compact
// state needed by later frames, so release the previous payload's reconstruction graph before parsing the next.
this.FrameInfo?.ReleaseOwner();
this.FrameInfo = null;
try
{
if (layeredImageIndex is null)
{
Av1BitStreamReader reader = new(buffer);
this.obuReader.ReadAll(ref reader, buffer.Length, this, false);
}
else
{
int layerOffset = 0;
for (int layer = 0; layer < Av1Constants.MaxSpatialLayerCount - 1 && layerOffset < buffer.Length; layer++)
{
uint declaredLayerSize = layer switch
{
0 => layeredImageIndex.Value.FirstLayerSize,
1 => layeredImageIndex.Value.SecondLayerSize,
_ => layeredImageIndex.Value.ThirdLayerSize
};
if (declaredLayerSize == 0)
{
break;
}
int layerSize = (int)declaredLayerSize;
Av1BitStreamReader layerReader = new(buffer.Slice(layerOffset, layerSize));
this.obuReader.ReadAll(ref layerReader, layerSize, this, false);
layerOffset += layerSize;
}
if (layerOffset < buffer.Length)
{
Span<byte> finalLayer = buffer[layerOffset..];
Av1BitStreamReader finalLayerReader = new(finalLayer);
this.obuReader.ReadAll(ref finalLayerReader, finalLayer.Length, this, false);
}
}
ObuSequenceHeader sequenceHeader = this.obuReader.SequenceHeader
?? throw new InvalidImageContentException("The AV1 payload contains no sequence header.");
if (requireShownFrame)
{
_ = this.referenceFrames.ResolveOutput();
}
// Preserve the effective CICP description used for conversion, including container values that legally
// supplied unspecified bitstream fields. This also exposes bitstream-only color metadata to callers.
ObuColorConfig effectiveColorConfig = sequenceHeader.ColorConfig;
return new CicpProfile(
(byte)effectiveColorConfig.ColorPrimaries,
(byte)effectiveColorConfig.TransferCharacteristics,
(byte)effectiveColorConfig.MatrixCoefficients,
effectiveColorConfig.ColorRange);
}
catch
{
// A failed frame may own pooled neighbor contexts while earlier layers own reconstructed references and
// published CDF snapshots. None can be reused after a non-transactional frame transition has failed.
this.frameDecodeState?.Dispose();
this.frameDecodeState = null;
this.obuReader.Reset();
this.entropyContexts?.Reset();
this.entropySequenceHeader = null;
this.SequenceHeader = null;
this.FrameHeader = null;
this.DecodedInterPredictionFeatures = Av1InterPredictionFeatures.None;
this.FrameInfo?.ReleaseOwner();
this.FrameInfo = null;
throw;
}
finally
{
// Validation inputs belong to this bounded decode call. Completed native buffers retain no references to
// either description, so releasing them here prevents a reused decoder from observing stale item state.
this.codecConfiguration = null;
this.containerColorProfile = null;
this.validatedSequenceHeader = null;
}
}
/// <summary>
/// Validates the active sequence against its container declarations before reconstruction begins.
/// </summary>
/// <param name="sequenceHeader">The active sequence header.</param>
private void ValidateSequence(ObuSequenceHeader sequenceHeader)
{
if (ReferenceEquals(this.validatedSequenceHeader, sequenceHeader))
{
return;
}
Av1FrameBuffer<byte>.ValidateDimensions(
sequenceHeader,
sequenceHeader.ColorConfig.GetColorFormat(),
false);
this.codecConfiguration?.Validate(sequenceHeader);
CicpProfile? colorProfile = this.containerColorProfile;
if (colorProfile is not null)
{
ObuColorConfig colorConfig = sequenceHeader.ColorConfig;
ObuColorPrimaries containerColorPrimaries = (ObuColorPrimaries)colorProfile.ColorPrimaries;
ObuTransferCharacteristics containerTransferCharacteristics = (ObuTransferCharacteristics)colorProfile.TransferCharacteristics;
ObuMatrixCoefficients containerMatrixCoefficients = (ObuMatrixCoefficients)colorProfile.MatrixCoefficients;
// AV1-ISOBMFF permits nclx to supply only bitstream fields explicitly coded as unspecified. A different
// specified value is a conformance error rather than a container-level color override.
if (colorConfig.ColorPrimaries == ObuColorPrimaries.Unspecified)
{
colorConfig.ColorPrimaries = containerColorPrimaries;
}
else if (colorConfig.ColorPrimaries != containerColorPrimaries)
{
throw new InvalidImageContentException("The HEIF CICP color primaries do not match the AV1 sequence header.");
}
if (colorConfig.TransferCharacteristics == ObuTransferCharacteristics.Unspecified)
{
colorConfig.TransferCharacteristics = containerTransferCharacteristics;
}
else if (colorConfig.TransferCharacteristics != containerTransferCharacteristics)
{
throw new InvalidImageContentException("The HEIF CICP transfer characteristics do not match the AV1 sequence header.");
}
if (colorConfig.MatrixCoefficients == ObuMatrixCoefficients.Unspecified)
{
colorConfig.MatrixCoefficients = containerMatrixCoefficients;
}
else if (colorConfig.MatrixCoefficients != containerMatrixCoefficients)
{
throw new InvalidImageContentException("The HEIF CICP matrix coefficients do not match the AV1 sequence header.");
}
if (colorConfig.ColorRange != colorProfile.FullRange)
{
throw new InvalidImageContentException("The HEIF CICP color range does not match the AV1 sequence header.");
}
}
// The same sequence header governs subsequent layered frames until another header OBU replaces it.
this.validatedSequenceHeader = sequenceHeader;
}
/// <summary>
/// Parses one entropy-coded tile payload into the current frame state.
/// </summary>
/// <param name="tileData">The entropy-coded tile payload.</param>
/// <param name="tileNum">The raster-order tile index.</param>
public void ReadTile(Span<byte> tileData, int tileNum)
{
FrameDecodeState frameDecodeState;
if (this.frameDecodeState is null)
{
ObuSequenceHeader sequenceHeader = this.obuReader.CurrentSequenceHeader;
ObuFrameHeader frameHeader = this.obuReader.CurrentFrameHeader;
this.ValidateSequence(sequenceHeader);
if (!ReferenceEquals(this.entropySequenceHeader, sequenceHeader))
{
if (this.entropySequenceHeader is not null)
{
// A coded-sequence boundary invalidates both sample references and their retained CDF snapshots.
// Returned snapshot graphs stay decoder-local and can be overwritten for the new sequence.
this.referenceFrames.Reset();
this.entropyContexts?.Reset();
}
this.entropySequenceHeader = sequenceHeader;
}
Av1FrameEntropyContext? primaryReferenceContext = null;
byte? primaryReferenceSlot = frameHeader.PrimaryReferenceSlot;
if (primaryReferenceSlot is not null)
{
// The uncompressed-header parser validates slot occupancy. Entropy ownership is checked here because
// only the reconstructed frame owner knows whether that slot retained a completed CDF snapshot.
Av1ReferenceFrame? primaryReference = this.referenceFrames.Resolve(primaryReferenceSlot.Value);
if (primaryReference is null || primaryReference.EntropyContext is null)
{
throw new InvalidImageContentException("The AV1 primary reference has no retained entropy context.");
}
primaryReferenceContext = primaryReference.EntropyContext;
}
// Every tile group in a frame contributes to the same mode-info and coefficient state.
Av1FrameEntropyContexts entropyContexts =
this.entropyContexts ??= new(frameHeader.QuantizationParameters.BaseQIndex);
Av1TileReader? tileReader = null;
Av1FrameBuffer<byte>? frameBuffer = null;
// Presentation-only samples contain no new tile syntax, so they keep the most recently reconstructed
// frame state. Release that state only when a new reconstruction begins to avoid overlapping two graphs.
this.FrameInfo?.ReleaseOwner();
this.FrameInfo = null;
try
{
tileReader = new Av1TileReader(
this.configuration,
sequenceHeader,
frameHeader,
entropyContexts,
primaryReferenceContext,
this.referenceFrames,
this.lumaPaletteColorIndexMap,
this.chromaPaletteColorIndexMap);
frameBuffer = new Av1FrameBuffer<byte>(
this.configuration,
sequenceHeader,
sequenceHeader.ColorConfig.GetColorFormat(),
false,
frameHeader.FrameSize.SuperResolutionUpscaledWidth,
frameHeader.FrameSize.FrameHeight)
{
Width = frameHeader.FrameSize.FrameWidth,
Height = frameHeader.FrameSize.FrameHeight
};
Av1FrameDecoder frameDecoder = new(
sequenceHeader,
frameHeader,
tileReader.FrameInfo,
frameBuffer,
this.referenceFrames,
new Av1TileReader.PaletteColorIndexMaps(
this.lumaPaletteColorIndexMap,
this.chromaPaletteColorIndexMap));
tileReader.FrameDecoder = frameDecoder;
frameDecodeState = new(tileReader, frameBuffer, frameDecoder);
this.frameDecodeState = frameDecodeState;
}
catch
{
frameBuffer?.Dispose();
tileReader?.Dispose();
throw;
}
}
else
{
frameDecodeState = this.frameDecodeState.Value;
}
frameDecodeState.TileReader.ReadTile(tileData, tileNum);
}
/// <summary>
/// Reconstructs a frame after all of its tile payloads have been parsed.
/// </summary>
public void CompleteFrame()
{
ObuSequenceHeader sequenceHeader = this.obuReader.SequenceHeader
?? throw new InvalidImageContentException("An AV1 frame cannot complete before its sequence header.");
ObuFrameHeader frameHeader = this.obuReader.FrameHeader
?? throw new InvalidImageContentException("An AV1 frame cannot complete before its frame header.");
Av1FrameBuffer<byte>? frameBuffer = null;
Av1FrameDecoder? frameDecoder = null;
Av1TileReader? tileReader = null;
Av1FrameBuffer<byte>? presentationBuffer = null;
try
{
this.ValidateSequence(sequenceHeader);
if (frameHeader.ShowExistingFrame)
{
Av1ReferenceFrame existingFrame = this.referenceFrames.ShowExisting((int)frameHeader.FrameToShowMapIdx);
ObuFrameHeader existingFrameHeader = existingFrame.FrameHeader;
if (existingFrameHeader.FrameType == ObuFrameType.KeyFrame)
{
// Both the decoder working context and the context retained by the newly aliased key frame reset
// frame. Later primary-reference selection must therefore observe normative defaults.
existingFrame.ResetEntropyContext();
this.entropyContexts?.Reset();
}
if (existingFrameHeader.FilmGrainParameters.ApplyGrain)
{
presentationBuffer = new Av1FrameBuffer<byte>(
this.configuration,
sequenceHeader,
existingFrame.FrameBuffer.ColorFormat,
false,
existingFrame.FrameBuffer.MaxWidth,
existingFrame.FrameBuffer.MaxHeight);
// Retained reference samples remain ungrained. Existing-frame presentation receives its own
// allocator-owned copy only when the inherited film-grain parameters actually modify the output.
existingFrame.FrameBuffer.CopyVisibleTo(presentationBuffer);
Av1FilmGrainDecoder filmGrainDecoder = new(sequenceHeader, existingFrameHeader, presentationBuffer);
filmGrainDecoder.DecodeFrame();
Av1ReferenceFrame presentationFrame = new(presentationBuffer, existingFrameHeader);
presentationBuffer = null;
this.referenceFrames.CommitOutput(presentationFrame);
}
this.SequenceHeader = sequenceHeader;
this.FrameHeader = existingFrameHeader;
return;
}
FrameDecodeState? activeFrameDecodeState = this.frameDecodeState;
if (activeFrameDecodeState is null)
{
throw new InvalidImageContentException("The AV1 frame completed without tile syntax.");
}
this.frameDecodeState = null;
FrameDecodeState activeFrame = activeFrameDecodeState.Value;
frameBuffer = activeFrame.FrameBuffer;
frameDecoder = activeFrame.FrameDecoder;
tileReader = activeFrame.TileReader;
Av1FrameInfo frameInfo = tileReader.FrameInfo;
Av1FrameBuffer<byte> reconstructedFrameBuffer = frameBuffer;
frameDecoder.CompleteFrame();
bool retainsReference = (frameHeader.RefreshFrameFlags & byte.MaxValue) != 0;
if (retainsReference)
{
// Motion compensation may address any clamped position inside the decoder border. Extending once after
// all in-loop filters lets every later block use the full padded span without per-prediction edge copies.
Av1ReferenceFrameBorder.Extend(reconstructedFrameBuffer);
// Detach only the state libaom retains on RefCntBuffer before any later ownership transfer can fail.
// The full reconstruction graph remains local to the current result and expires independently.
frameInfo.PrepareReferenceState();
}
bool needsSeparatePresentation = frameHeader.ShowFrame && frameHeader.FilmGrainParameters.ApplyGrain && retainsReference;
if (needsSeparatePresentation)
{
presentationBuffer = new Av1FrameBuffer<byte>(
this.configuration,
sequenceHeader,
reconstructedFrameBuffer.ColorFormat,
false,
reconstructedFrameBuffer.MaxWidth,
reconstructedFrameBuffer.MaxHeight);
// Film grain must never contaminate a decoded reference. A shown frame that is also refreshed therefore
// receives one allocator-owned presentation copy; frames with no reference role are grained in place.
reconstructedFrameBuffer.CopyVisibleTo(presentationBuffer);
}
Av1FrameBuffer<byte> grainTarget = presentationBuffer ?? reconstructedFrameBuffer;
if (frameHeader.ShowFrame && frameHeader.FilmGrainParameters.ApplyGrain)
{
Av1FilmGrainDecoder filmGrainDecoder = new(sequenceHeader, frameHeader, grainTarget);
filmGrainDecoder.DecodeFrame();
}
Av1ReferenceFrame referenceFrame;
if (retainsReference)
{
Av1FrameEntropyContexts entropyContexts = tileReader.EntropyContexts;
Av1FrameEntropyContext entropySnapshot = entropyContexts.RentPublishedSnapshot();
try
{
referenceFrame = new(reconstructedFrameBuffer, frameHeader, frameInfo, entropySnapshot, entropyContexts);
}
catch
{
// The snapshot rent precedes the reference owner. Return it if object construction cannot accept it.
entropyContexts.ReturnSnapshot(entropySnapshot);
throw;
}
}
else
{
// Presentation-only frames can never become primary references, so they own no unused CDF graph.
referenceFrame = new(reconstructedFrameBuffer, frameHeader);
}
frameBuffer = null;
if (!this.referenceFrames.Commit(frameHeader.RefreshFrameFlags, referenceFrame, frameHeader.ShowFrame && !needsSeparatePresentation))
{
referenceFrame.Dispose();
}
if (presentationBuffer is not null)
{
Av1ReferenceFrame presentationFrame = new(presentationBuffer, frameHeader);
presentationBuffer = null;
this.referenceFrames.CommitOutput(presentationFrame);
}
this.SequenceHeader = sequenceHeader;
this.FrameHeader = frameHeader;
this.DecodedInterPredictionFeatures |= frameInfo.InterPredictionFeatures;
// Hidden frames can contain the inter syntax needed to validate a sequence. Retain only the latest full
// reconstruction state until the next bounded decode; reference-map entries keep their compact state.
frameInfo.AddOwner();
this.FrameInfo?.ReleaseOwner();
this.FrameInfo = frameInfo;
}
finally
{
// A non-shown frame or failed reconstruction never escapes this callback. The tile reader releases the
// reconstruction lease; a retained frame keeps only its compact reference state after neighbor contexts
// and the remaining frame-sized syntax are returned.
frameDecoder?.Dispose();
presentationBuffer?.Dispose();
frameBuffer?.Dispose();
tileReader?.Dispose();
this.frameDecodeState?.Dispose();
this.frameDecodeState = null;
}
}
/// <summary>
/// Releases the current tile parser, reference map, and retained presentation output.
/// </summary>
public void Dispose()
{
this.frameDecodeState?.Dispose();
this.frameDecodeState = null;
this.referenceFrames.Dispose();
this.FrameInfo?.ReleaseOwner();
this.FrameInfo = null;
this.lumaPaletteColorIndexMap.Dispose();
this.chromaPaletteColorIndexMap.Dispose();
}
/// <summary>
/// Carries the active frame resources as one valid state so no partially initialized combination can be observed.
/// </summary>
private readonly struct FrameDecodeState(
Av1TileReader tileReader,
Av1FrameBuffer<byte> frameBuffer,
Av1FrameDecoder frameDecoder) : IDisposable
{
/// <summary>
/// Gets the tile parser shared by all tile groups in the frame.
/// </summary>
public Av1TileReader TileReader { get; } = tileReader;
/// <summary>
/// Gets the destination sample buffer reconstructed by the frame pipeline.
/// </summary>
public Av1FrameBuffer<byte> FrameBuffer { get; } = frameBuffer;
/// <summary>
/// Gets the reconstruction pipeline for the frame.
/// </summary>
public Av1FrameDecoder FrameDecoder { get; } = frameDecoder;
/// <summary>
/// Releases every resource when ownership has not transferred to a completed frame.
/// </summary>
public void Dispose()
{
this.FrameDecoder.Dispose();
this.FrameBuffer.Dispose();
this.TileReader.Dispose();
}
}
}

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

@ -0,0 +1,654 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Owns the padded luma and chroma sample planes for one decoded AV1 frame.
/// </summary>
/// <typeparam name="T">The unmanaged storage-element type used by the plane allocations.</typeparam>
internal sealed class Av1FrameBuffer<T> : IDisposable
where T : unmanaged
{
/// <summary>
/// The number of luma border samples reserved for prediction and in-loop filtering.
/// </summary>
// Scaled prediction may start 284 luma samples outside a retained frame and then consume three preceding filter
// taps. The normative 288-sample border keeps that entire source window directly addressable without block copies.
public const int DecoderPaddingValue = 288;
/// <summary>
/// The allocation-mask bit for the luma plane.
/// </summary>
private const int PictureBufferYFlag = 1 << 0;
/// <summary>
/// The allocation-mask bit for the first chroma plane.
/// </summary>
private const int PictureBufferCbFlag = 1 << 1;
/// <summary>
/// The allocation-mask bit for the second chroma plane.
/// </summary>
private const int PictureBufferCrFlag = 1 << 2;
/// <summary>
/// The allocation mask for a monochrome frame.
/// </summary>
private const int PictureBufferLumaMask = PictureBufferYFlag;
/// <summary>
/// The allocation mask for a frame containing all three planes.
/// </summary>
private const int PictureBufferFullMask = PictureBufferYFlag | PictureBufferCbFlag | PictureBufferCrFlag;
/// <summary>
/// The number of <typeparamref name="T"/> elements occupied by one logical sample.
/// </summary>
private readonly int storageElementsPerSample;
/// <summary>
/// The complete plane ownership state, or <see langword="null"/> after disposal.
/// </summary>
private FramePlanes? planes;
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameBuffer{T}"/> class at sequence-maximum dimensions.
/// </summary>
/// <param name="configuration">The configuration providing the plane allocator.</param>
/// <param name="sequenceHeader">The sequence header defining maximum dimensions, bit depth, and chroma layout.</param>
/// <param name="maxColorFormat">The maximum color format to allocate for a non-monochrome sequence.</param>
/// <param name="is16BitPipeline">Indicates whether reconstruction uses native 16-bit sample storage.</param>
/// <exception cref="InvalidImageContentException">The padded frame planes cannot be represented as contiguous allocations.</exception>
public Av1FrameBuffer(Configuration configuration, ObuSequenceHeader sequenceHeader, Av1ColorFormat maxColorFormat, bool is16BitPipeline)
: this(
configuration,
sequenceHeader,
maxColorFormat,
is16BitPipeline,
sequenceHeader.MaxFrameWidth,
sequenceHeader.MaxFrameHeight)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameBuffer{T}"/> class for one active frame allocation.
/// </summary>
/// <param name="configuration">The configuration providing the plane allocator.</param>
/// <param name="sequenceHeader">The sequence header defining bit depth and chroma layout.</param>
/// <param name="maxColorFormat">The color format to allocate for a non-monochrome sequence.</param>
/// <param name="is16BitPipeline">Indicates whether reconstruction uses native 16-bit sample storage.</param>
/// <param name="allocationWidth">The padded plane's active luma width before decoder borders.</param>
/// <param name="allocationHeight">The padded plane's active luma height before decoder borders.</param>
/// <exception cref="InvalidImageContentException">The padded frame planes cannot be represented as contiguous allocations.</exception>
public Av1FrameBuffer(
Configuration configuration,
ObuSequenceHeader sequenceHeader,
Av1ColorFormat maxColorFormat,
bool is16BitPipeline,
int allocationWidth,
int allocationHeight)
{
ValidateDimensions(sequenceHeader, maxColorFormat, is16BitPipeline);
this.MemoryAllocator = configuration.MemoryAllocator;
Av1ColorFormat colorFormat = sequenceHeader.ColorConfig.IsMonochrome ? Av1ColorFormat.Yuv400 : maxColorFormat;
this.MaxWidth = allocationWidth;
this.MaxHeight = allocationHeight;
this.BitDepth = sequenceHeader.ColorConfig.BitDepth;
this.ColorConfig = sequenceHeader.ColorConfig;
this.BytesPerSample = this.BitDepth > Av1BitDepth.EightBit || is16BitPipeline ? 2 : 1;
this.storageElementsPerSample = Math.Max(
(this.BytesPerSample + Unsafe.SizeOf<T>() - 1) / Unsafe.SizeOf<T>(),
1);
this.ColorFormat = colorFormat;
this.Is16BitPipeline = is16BitPipeline;
int bufferEnableMask = sequenceHeader.ColorConfig.IsMonochrome ? PictureBufferLumaMask : PictureBufferFullMask;
int leftPadding = DecoderPaddingValue;
int rightPadding = DecoderPaddingValue;
int topPadding = DecoderPaddingValue;
int bottomPadding = DecoderPaddingValue;
this.StartPosition = new Point(leftPadding, topPadding);
this.Width = this.MaxWidth;
this.Height = this.MaxHeight;
int strideY = this.MaxWidth + leftPadding + rightPadding;
int heightY = this.MaxHeight + topPadding + bottomPadding;
this.OriginX = leftPadding;
this.OriginY = topPadding;
int strideChroma = 0;
int heightChroma = 0;
switch (this.ColorFormat)
{
case Av1ColorFormat.Yuv420:
strideChroma = (strideY + 1) >> 1;
heightChroma = (heightY + 1) >> 1;
break;
case Av1ColorFormat.Yuv422:
strideChroma = (strideY + 1) >> 1;
heightChroma = heightY;
break;
case Av1ColorFormat.Yuv444:
strideChroma = strideY;
heightChroma = heightY;
break;
}
long lumaElementCount = (long)strideY * this.storageElementsPerSample * heightY;
long chromaElementCount = (long)strideChroma * this.storageElementsPerSample * heightChroma;
bool planesExceedContiguousLimit =
lumaElementCount >= int.MaxValue ||
(bufferEnableMask == PictureBufferFullMask && chromaElementCount >= int.MaxValue);
if (planesExceedContiguousLimit)
{
// The reconstruction operators use one span plus a constant stride to address padded neighbors. Reject an
// external geometry that cannot satisfy that ownership contract before Allocate2D falls back to groups.
throw new InvalidImageContentException("The AV1 frame dimensions exceed the contiguous decoder plane limit.");
}
// Block reconstruction and the SIMD predictors address decoder padding through one span plus a constant row
// stride. Establish that invariant at the plane owner instead of copying fragmented groups in every hot path.
Buffer2D<T> luma = configuration.MemoryAllocator.Allocate2D<T>(
strideY * this.storageElementsPerSample,
heightY,
preferContiguosImageBuffers: true);
Buffer2D<T>? chromaBlue = null;
Buffer2D<T>? chromaRed = null;
try
{
ChromaPlanes? chroma = null;
if (bufferEnableMask == PictureBufferFullMask)
{
chromaBlue = configuration.MemoryAllocator.Allocate2D<T>(
strideChroma * this.storageElementsPerSample,
heightChroma,
preferContiguosImageBuffers: true);
chromaRed = configuration.MemoryAllocator.Allocate2D<T>(
strideChroma * this.storageElementsPerSample,
heightChroma,
preferContiguosImageBuffers: true);
chroma = new ChromaPlanes(chromaBlue, chromaRed);
}
this.planes = new(luma, chroma);
}
catch
{
// Construction publishes the owner only after every required plane has been rented. Release earlier planes
// here because a later allocation failure leaves no constructed frame buffer for the caller to dispose.
chromaRed?.Dispose();
chromaBlue?.Dispose();
luma.Dispose();
throw;
}
}
/// <summary>
/// Gets the padded luma-coordinate origin of the visible frame.
/// </summary>
public Point StartPosition { get; private set; }
/// <summary>
/// Gets the Y luma buffer.
/// </summary>
public Buffer2D<T>? BufferY => this.planes?.Luma;
/// <summary>
/// Gets the U chroma buffer.
/// </summary>
public Buffer2D<T>? BufferCb => this.planes?.Chroma?.Blue;
/// <summary>
/// Gets the V chroma buffer.
/// </summary>
public Buffer2D<T>? BufferCr => this.planes?.Chroma?.Red;
/// <summary>
/// Gets or sets the horizontal padding distance.
/// </summary>
public int OriginX { get; set; }
/// <summary>
/// Gets or sets the vertical padding distance.
/// </summary>
public int OriginY { get; set; }
/// <summary>
/// Gets or sets the luma picture width, excluding padding.
/// </summary>
public int Width { get; set; }
/// <summary>
/// Gets or sets the luma picture height, excluding padding.
/// </summary>
public int Height { get; set; }
/// <summary>
/// Gets or sets the maximum luma picture width.
/// </summary>
public int MaxWidth { get; set; }
/// <summary>
/// Gets or sets the pixel bit depth.
/// </summary>
public Av1BitDepth BitDepth { get; set; }
/// <summary>
/// Gets the number of bytes used to store each reconstructed sample.
/// </summary>
public int BytesPerSample { get; }
/// <summary>
/// Gets the color configuration signaled by the AV1 sequence header.
/// </summary>
public ObuColorConfig ColorConfig { get; }
/// <summary>
/// Gets or sets the luma and chroma plane sampling layout.
/// </summary>
public Av1ColorFormat ColorFormat { get; set; }
/// <summary>
/// Gets or sets the maximum luma picture height.
/// </summary>
public int MaxHeight { get; set; }
/// <summary>
/// Gets a value indicating whether reconstruction uses native 16-bit samples.
/// </summary>
public bool Is16BitPipeline { get; }
/// <summary>
/// Gets the allocator used for frame-owned and frame-scoped working buffers.
/// </summary>
public MemoryAllocator MemoryAllocator { get; }
/// <summary>
/// Validates that the maximum sequence planes fit the decoder's contiguous ownership contract.
/// </summary>
/// <param name="sequenceHeader">The sequence header defining maximum dimensions, bit depth, and chroma layout.</param>
/// <param name="maxColorFormat">The maximum color format required by the sequence.</param>
/// <param name="is16BitPipeline">Indicates whether reconstruction uses native 16-bit sample storage.</param>
public static void ValidateDimensions(
ObuSequenceHeader sequenceHeader,
Av1ColorFormat maxColorFormat,
bool is16BitPipeline)
{
int bytesPerSample = sequenceHeader.ColorConfig.BitDepth > Av1BitDepth.EightBit || is16BitPipeline ? 2 : 1;
int storageElementsPerSample = Math.Max(
(bytesPerSample + Unsafe.SizeOf<T>() - 1) / Unsafe.SizeOf<T>(),
1);
long strideY = (long)sequenceHeader.MaxFrameWidth + (DecoderPaddingValue * 2L);
long heightY = (long)sequenceHeader.MaxFrameHeight + (DecoderPaddingValue * 2L);
Av1ColorFormat colorFormat = sequenceHeader.ColorConfig.IsMonochrome ? Av1ColorFormat.Yuv400 : maxColorFormat;
long strideChroma = 0;
long heightChroma = 0;
switch (colorFormat)
{
case Av1ColorFormat.Yuv420:
strideChroma = (strideY + 1) >> 1;
heightChroma = (heightY + 1) >> 1;
break;
case Av1ColorFormat.Yuv422:
strideChroma = (strideY + 1) >> 1;
heightChroma = heightY;
break;
case Av1ColorFormat.Yuv444:
strideChroma = strideY;
heightChroma = heightY;
break;
}
long lumaElementCount = strideY * storageElementsPerSample * heightY;
long chromaElementCount = strideChroma * storageElementsPerSample * heightChroma;
if (lumaElementCount >= int.MaxValue ||
(!sequenceHeader.ColorConfig.IsMonochrome && chromaElementCount >= int.MaxValue))
{
// Every decoder operator addresses padding through one contiguous span. Reject external sequence geometry
// before frame-wide syntax state is allocated so hostile dimensions cannot bypass allocator limits.
throw new InvalidImageContentException("The AV1 frame dimensions exceed the contiguous decoder plane limit.");
}
}
/// <summary>
/// Gets the padded storage allocation for one component plane.
/// </summary>
/// <param name="plane">The requested component plane.</param>
/// <returns>The requested plane allocation.</returns>
public Buffer2D<T> GetPlaneBuffer(Av1Plane plane)
{
this.GetPlaneLayout(plane, 0, 0, out Buffer2D<T> buffer, out _, out _, out _, out _);
return buffer;
}
/// <summary>
/// Copies the visible sample planes and active picture geometry to another compatible frame buffer.
/// </summary>
/// <param name="destination">The frame buffer receiving the copied reconstruction.</param>
public void CopyVisibleTo(Av1FrameBuffer<T> destination)
{
destination.StartPosition = this.StartPosition;
destination.OriginX = this.OriginX;
destination.OriginY = this.OriginY;
destination.Width = this.Width;
destination.Height = this.Height;
destination.MaxWidth = this.MaxWidth;
destination.MaxHeight = this.MaxHeight;
destination.BitDepth = this.BitDepth;
destination.ColorFormat = this.ColorFormat;
int planeCount = this.ColorFormat == Av1ColorFormat.Yuv400 ? 1 : 3;
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++)
{
Av1Plane plane = (Av1Plane)planeIndex;
int subX = plane != Av1Plane.Y && this.ColorFormat is Av1ColorFormat.Yuv420 or Av1ColorFormat.Yuv422 ? 1 : 0;
int subY = plane != Av1Plane.Y && this.ColorFormat == Av1ColorFormat.Yuv420 ? 1 : 0;
this.GetPlaneLayout(
plane,
subX,
subY,
out Buffer2D<T> sourceBuffer,
out int sourceOriginX,
out int sourceOriginY,
out int width,
out int height);
destination.GetPlaneLayout(
plane,
subX,
subY,
out Buffer2D<T> destinationBuffer,
out int destinationOriginX,
out int destinationOriginY,
out _,
out _);
int storageWidth = width * this.storageElementsPerSample;
int sourceStorageX = sourceOriginX * this.storageElementsPerSample;
int destinationStorageX = destinationOriginX * this.storageElementsPerSample;
// A film-grain presentation owns only the active picture. Grain synthesis creates its odd-edge
// extension before reading it, so copying reference borders or unused sequence-sized storage is waste.
for (int row = 0; row < height; row++)
{
sourceBuffer.DangerousGetRowSpan(sourceOriginY + row)
.Slice(sourceStorageX, storageWidth)
.CopyTo(destinationBuffer.DangerousGetRowSpan(destinationOriginY + row).Slice(destinationStorageX, storageWidth));
}
}
}
/// <summary>
/// Releases the owned luma and chroma plane allocations.
/// </summary>
public void Dispose()
{
FramePlanes? ownedPlanes = this.planes;
this.planes = null;
if (ownedPlanes is null)
{
return;
}
FramePlanes activePlanes = ownedPlanes.Value;
activePlanes.Luma.Dispose();
ChromaPlanes? chroma = activePlanes.Chroma;
if (chroma is not null)
{
chroma.Value.Blue.Dispose();
chroma.Value.Red.Dispose();
}
}
/// <summary>
/// Gets a storage-element span beginning one logical row before a block.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="locationInPixels">The block origin in plane samples.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <param name="stride">Receives the logical samples between adjacent rows.</param>
/// <returns>The span beginning one logical row before the block.</returns>
public Span<T> DeriveBlockPointer(Av1Plane plane, Point locationInPixels, int subX, int subY, out int stride)
{
this.GetPlaneLayout(
plane,
subX,
subY,
out Buffer2D<T> buffer,
out int originX,
out int originY,
out _,
out _);
int elementStride = buffer.Width;
stride = elementStride / this.storageElementsPerSample;
int blockOffset = (((originY + locationInPixels.Y) * stride) + originX + locationInPixels.X) *
this.storageElementsPerSample;
// Intra prediction addresses above neighbors relative to the destination span, so index zero is the previous row.
blockOffset -= elementStride;
Guard.MustBeGreaterThanOrEqualTo(blockOffset, 0, nameof(blockOffset));
return buffer.DangerousGetSingleSpan()[blockOffset..];
}
/// <summary>
/// Gets a native 16-bit sample span beginning one logical row before a block.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="locationInPixels">The block origin in plane samples.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <param name="stride">Receives the logical samples between adjacent rows.</param>
/// <returns>The 16-bit span beginning one logical row before the block.</returns>
public Span<short> DeriveBlockPointer16(Av1Plane plane, Point locationInPixels, int subX, int subY, out int stride)
{
this.GetPlaneLayout(
plane,
subX,
subY,
out Buffer2D<T> buffer,
out int originX,
out int originY,
out _,
out _);
stride = buffer.Width / this.storageElementsPerSample;
int blockOffset = ((originY + locationInPixels.Y - 1) * stride) + originX + locationInPixels.X;
Guard.MustBeGreaterThanOrEqualTo(blockOffset, 0, nameof(blockOffset));
// High-bit-depth reconstruction uses native 16-bit samples in the byte-backed frame planes.
return MemoryMarshal.Cast<T, short>(buffer.DangerousGetSingleSpan())[blockOffset..];
}
/// <summary>
/// Gets the visible sample region for one plane.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <returns>The plane region excluding decoder padding.</returns>
public Buffer2DRegion<T> DeriveBlockPointer(Av1Plane plane, int subX, int subY)
{
this.GetPlaneLayout(
plane,
subX,
subY,
out Buffer2D<T> buffer,
out int originX,
out int originY,
out int width,
out int height);
Rectangle region = new(
originX * this.storageElementsPerSample,
originY,
width * this.storageElementsPerSample,
height);
return new Buffer2DRegion<T>(buffer, region);
}
/// <summary>
/// Gets one visible row of native 16-bit samples from a plane.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="row">The zero-based visible row index.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <returns>The visible row without decoder padding.</returns>
public Span<ushort> GetHighBitDepthRowSpan(Av1Plane plane, int row, int subX, int subY)
{
this.GetPlaneLayout(
plane,
subX,
subY,
out Buffer2D<T> buffer,
out int originX,
out int originY,
out int width,
out _);
Span<ushort> samples = MemoryMarshal.Cast<T, ushort>(buffer.DangerousGetRowSpan(originY + row));
return samples.Slice(originX, width);
}
/// <summary>
/// Gets the complete padded storage allocation for one plane.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <param name="stride">Receives the number of logical samples between adjacent rows.</param>
/// <param name="origin">Receives the visible plane origin within the padded allocation.</param>
/// <returns>The complete plane allocation, including decoder padding.</returns>
public Span<T> GetPaddedPlaneSpan(Av1Plane plane, int subX, int subY, out int stride, out Point origin)
{
this.GetPlaneLayout(
plane,
subX,
subY,
out Buffer2D<T> buffer,
out int originX,
out int originY,
out _,
out _);
stride = buffer.Width / this.storageElementsPerSample;
origin = new(originX, originY);
return buffer.DangerousGetSingleSpan();
}
/// <summary>
/// Gets the complete padded storage allocation for one native 16-bit plane.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <param name="stride">Receives the number of logical samples between adjacent rows.</param>
/// <param name="origin">Receives the visible plane origin within the padded allocation.</param>
/// <returns>The complete plane allocation, including decoder padding.</returns>
public Span<ushort> GetPaddedPlaneSpan16(Av1Plane plane, int subX, int subY, out int stride, out Point origin)
=> MemoryMarshal.Cast<T, ushort>(this.GetPaddedPlaneSpan(plane, subX, subY, out stride, out origin));
/// <summary>
/// Resolves a plane allocation and its visible padded layout.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <param name="buffer">Receives the selected plane allocation.</param>
/// <param name="originX">Receives the horizontal visible origin in plane samples.</param>
/// <param name="originY">Receives the vertical visible origin in plane samples.</param>
/// <param name="width">Receives the visible plane width.</param>
/// <param name="height">Receives the visible plane height.</param>
private void GetPlaneLayout(
Av1Plane plane,
int subX,
int subY,
out Buffer2D<T> buffer,
out int originX,
out int originY,
out int width,
out int height)
{
FramePlanes? ownedPlanes = this.planes;
ObjectDisposedException.ThrowIf(ownedPlanes is null, this);
FramePlanes activePlanes = ownedPlanes.Value;
switch (plane)
{
case Av1Plane.Y:
buffer = activePlanes.Luma;
originX = this.OriginX;
originY = this.OriginY;
width = this.Width;
height = this.Height;
break;
case Av1Plane.U:
buffer = activePlanes.Chroma?.Blue
?? throw new InvalidOperationException("A monochrome AV1 frame has no blue-difference plane.");
originX = this.OriginX >> subX;
originY = this.OriginY >> subY;
width = Av1Math.DivideLog2Ceiling(this.Width, subX);
height = Av1Math.DivideLog2Ceiling(this.Height, subY);
break;
case Av1Plane.V:
default:
buffer = activePlanes.Chroma?.Red
?? throw new InvalidOperationException("A monochrome AV1 frame has no red-difference plane.");
originX = this.OriginX >> subX;
originY = this.OriginY >> subY;
width = Av1Math.DivideLog2Ceiling(this.Width, subX);
height = Av1Math.DivideLog2Ceiling(this.Height, subY);
break;
}
}
/// <summary>
/// Carries the mandatory luma owner and the optional complete chroma pair as one state.
/// </summary>
private readonly struct FramePlanes(Buffer2D<T> luma, ChromaPlanes? chroma)
{
/// <summary>
/// Gets the padded luma plane.
/// </summary>
public Buffer2D<T> Luma { get; } = luma;
/// <summary>
/// Gets the padded chroma planes when the frame contains chroma.
/// </summary>
public ChromaPlanes? Chroma { get; } = chroma;
}
private readonly struct ChromaPlanes(Buffer2D<T> blue, Buffer2D<T> red)
{
/// <summary>
/// Gets the padded blue-difference plane.
/// </summary>
public Buffer2D<T> Blue { get; } = blue;
/// <summary>
/// Gets the padded red-difference plane.
/// </summary>
public Buffer2D<T> Red { get; } = red;
}
}

21
src/ImageSharp/Formats/Heif/Av1/Av1LayerSelector.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Identifies the AV1 spatial layer selected by an AVIF image item.
/// </summary>
/// <param name="layerId">The spatial-layer identifier, or <see cref="AllLayers"/> for progressive or final-layer decoding.</param>
internal readonly struct Av1LayerSelector(ushort layerId)
{
/// <summary>
/// The layer identifier that selects progressive exposure or the final layer rather than one specific spatial layer.
/// </summary>
public const ushort AllLayers = ushort.MaxValue;
/// <summary>
/// Gets the spatial-layer identifier, or <see cref="AllLayers"/> when no individual layer is selected.
/// </summary>
public ushort LayerId { get; } = layerId;
}

97
src/ImageSharp/Formats/Heif/Av1/Av1LayeredImageIndex.cs

@ -0,0 +1,97 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Describes the explicit payload sizes that delimit the first three layers of a layered AV1 image item.
/// </summary>
/// <param name="firstLayerSize">The first layer size in bytes.</param>
/// <param name="secondLayerSize">The second layer size in bytes.</param>
/// <param name="thirdLayerSize">The third layer size in bytes.</param>
internal readonly struct Av1LayeredImageIndex(uint firstLayerSize, uint secondLayerSize, uint thirdLayerSize)
{
/// <summary>
/// Gets the first layer size in bytes.
/// </summary>
public uint FirstLayerSize { get; } = firstLayerSize;
/// <summary>
/// Gets the second layer size in bytes.
/// </summary>
public uint SecondLayerSize { get; } = secondLayerSize;
/// <summary>
/// Gets the third layer size in bytes.
/// </summary>
public uint ThirdLayerSize { get; } = thirdLayerSize;
/// <summary>
/// Gets the number of item bytes needed to decode the selected spatial layer.
/// </summary>
/// <param name="itemSize">The complete logical image-item payload size.</param>
/// <param name="selector">The requested spatial layer, or <see langword="null"/> to decode the final layer.</param>
/// <returns>The cumulative payload size through the selected layer, or the complete item size for final-layer decoding.</returns>
public int GetPayloadLength(int itemSize, Av1LayerSelector? selector)
{
int selectedLayer = selector is null || selector.Value.LayerId == Av1LayerSelector.AllLayers
? -1
: selector.Value.LayerId;
uint remainingSize = (uint)itemSize;
uint selectedPayloadSize = 0;
int layerCount = 0;
for (int layer = 0; layer < Av1Constants.MaxSpatialLayerCount - 1; layer++)
{
uint layerSize = layer switch
{
0 => this.FirstLayerSize,
1 => this.SecondLayerSize,
_ => this.ThirdLayerSize
};
layerCount++;
if (layerSize == 0)
{
if (selectedLayer < 0 || selectedLayer == layer)
{
selectedPayloadSize += remainingSize;
}
remainingSize = 0;
break;
}
if (layerSize >= remainingSize)
{
// Every explicit layer must leave at least one byte for the final implicit layer. A zero entry instead
// identifies the current layer as final and consumes the complete remainder.
throw new InvalidImageContentException($"AV1 layered-image layer {layer} does not fit within the item payload.");
}
if (selectedLayer < 0 || layer <= selectedLayer)
{
selectedPayloadSize += layerSize;
}
remainingSize -= layerSize;
}
if (remainingSize != 0)
{
if (selectedLayer < 0 || selectedLayer == layerCount)
{
selectedPayloadSize += remainingSize;
}
layerCount++;
}
if (selectedLayer >= layerCount)
{
throw new InvalidImageContentException($"AV1 layer selector requests layer {selectedLayer}, but the item contains {layerCount} layers.");
}
return selectedLayer < 0 ? itemSize : (int)selectedPayloadSize;
}
}

309
src/ImageSharp/Formats/Heif/Av1/Av1Math.cs

@ -0,0 +1,309 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Provides the integer arithmetic primitives used by AV1 syntax and reconstruction.
/// </summary>
internal static class Av1Math
{
/// <summary>
/// Gets the zero-based position of the most significant set bit.
/// </summary>
/// <param name="value">A nonzero unsigned value.</param>
/// <returns>The most significant set-bit position.</returns>
public static int MostSignificantBit(uint value)
{
int log = 0;
int i;
Guard.IsTrue(value != 0, nameof(value), "Must have at least one bit set.");
for (i = 4; i >= 0; --i)
{
int shift = 1 << i;
uint x = value >> shift;
if (x != 0)
{
value = x;
log += shift;
}
}
return log;
}
/// <summary>
/// Gets the integer base-two logarithm of a positive value.
/// </summary>
/// <param name="n">The value.</param>
/// <returns>The zero-based position of the most significant set bit.</returns>
public static int Log2(int n)
{
int result = 0;
while ((n >>= 1) > 0)
{
result++;
}
return result;
}
/// <summary>
/// Gets the integer base-two logarithm of an unsigned 32-bit value.
/// </summary>
/// <param name="x">The value.</param>
/// <returns>The zero-based position of the most significant set bit.</returns>
public static uint Log2_32(uint x)
{
uint log = 0;
int i;
for (i = 4; i >= 0; --i)
{
uint shift = 1u << i;
uint n = x >> (int)shift;
if (n != 0)
{
x = n;
log += shift;
}
}
return log;
}
/// <summary>
/// Gets the greatest integer less than or equal to the base-two logarithm of a nonzero value.
/// </summary>
/// <param name="value">The nonzero value.</param>
/// <returns>The floor of the base-two logarithm.</returns>
public static uint FloorLog2(uint value)
{
uint s = 0;
while (value != 0U)
{
value >>= 1;
s++;
}
return s - 1;
}
/// <summary>
/// Gets the least integer greater than or equal to the base-two logarithm of a value.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>The ceiling of the base-two logarithm, or zero for values below two.</returns>
public static uint CeilLog2(uint value)
{
if (value < 2)
{
return 0;
}
uint i = 1;
uint p = 2;
while (p < value)
{
i++;
p <<= 1;
}
return i;
}
/// <summary>
/// Clips an unsigned sample to the range represented by a bit depth.
/// </summary>
/// <param name="value">The sample value.</param>
/// <param name="bitDepth">The number of sample bits.</param>
/// <returns>The clipped sample.</returns>
public static uint Clip1(uint value, int bitDepth) =>
Clip3(0, (1U << bitDepth) - 1, value);
/// <summary>
/// Clips an unsigned value to an inclusive range.
/// </summary>
/// <param name="min">The inclusive lower bound.</param>
/// <param name="max">The inclusive upper bound.</param>
/// <param name="value">The value to clip.</param>
/// <returns>The clipped value.</returns>
public static uint Clip3(uint min, uint max, uint value) => Math.Max(min, Math.Min(max, value));
/// <summary>
/// Clips a signed value to an inclusive range.
/// </summary>
/// <param name="min">The inclusive lower bound.</param>
/// <param name="max">The inclusive upper bound.</param>
/// <param name="value">The value to clip.</param>
/// <returns>The clipped value.</returns>
public static int Clip3(int min, int max, int value) => Math.Max(min, Math.Min(max, value));
/// <summary>
/// Divides an unsigned value by a power of two with nearest-integer rounding.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="n">The base-two divisor exponent.</param>
/// <returns>The rounded quotient.</returns>
public static uint Round2(uint value, int n)
{
if (n == 0)
{
return value;
}
return (uint)((value + (1 << (n - 1))) >> n);
}
/// <summary>
/// Divides the absolute magnitude of a signed value by a power of two with nearest-integer rounding.
/// </summary>
/// <param name="value">The signed value.</param>
/// <param name="n">The base-two divisor exponent.</param>
/// <returns>The rounded nonnegative magnitude.</returns>
public static int Round2(int value, int n)
{
if (value < 0)
{
value = -value;
}
return (int)Round2((uint)value, n);
}
/// <summary>
/// Aligns a value upward to a multiple of a power of two.
/// </summary>
/// <param name="value">The value to align.</param>
/// <param name="n">The base-two alignment exponent.</param>
/// <returns>The aligned value.</returns>
public static int AlignPowerOf2(int value, int n)
{
int mask = (1 << n) - 1;
return (value + mask) & ~mask;
}
/// <summary>
/// Divides a value by a power of two with nearest-integer rounding.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="n">The base-two divisor exponent.</param>
/// <returns>The rounded quotient.</returns>
public static int RoundPowerOf2(int value, int n) => (value + ((1 << n) >> 1)) >> n;
/// <summary>
/// Clamps a signed integer to an inclusive range.
/// </summary>
/// <param name="value">The value to clamp.</param>
/// <param name="low">The inclusive lower bound.</param>
/// <param name="high">The inclusive upper bound.</param>
/// <returns>The clamped value.</returns>
public static int Clamp(int value, int low, int high)
=> Math.Max(low, Math.Min(high, value));
/// <summary>
/// Clamps a signed long integer to an inclusive range.
/// </summary>
/// <param name="value">The value to clamp.</param>
/// <param name="low">The inclusive lower bound.</param>
/// <param name="high">The inclusive upper bound.</param>
/// <returns>The clamped value.</returns>
public static long Clamp(long value, long low, long high)
=> Math.Max(low, Math.Min(high, value));
/// <summary>
/// Divides a value by a power of two with floor rounding.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="n">The base-two divisor exponent.</param>
/// <returns>The floor-rounded quotient.</returns>
public static int DivideLog2Floor(int value, int n)
=> value >> n;
/// <summary>
/// Divides a nonnegative value by a power of two with ceiling rounding.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="n">The base-two divisor exponent.</param>
/// <returns>The ceiling-rounded quotient.</returns>
public static int DivideLog2Ceiling(int value, int n)
=> (value + (1 << n) - 1) >> n;
/// <summary>
/// Divides a value by a power of two with nearest-integer rounding.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="bitCount">The base-two divisor exponent.</param>
/// <returns>The rounded quotient.</returns>
public static int DivideRound(int value, int bitCount)
=> (value + (1 << (bitCount - 1))) >> bitCount;
/// <summary>
/// Gets the nonnegative remainder after division by eight.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>The low three bits of the value.</returns>
public static int Modulus8(int value) => value & 0x07;
/// <summary>
/// Divides a value by eight with floor rounding.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>The floor-rounded quotient.</returns>
public static int DivideBy8Floor(int value) => value >> 3;
/// <summary>
/// Divides a signed value by a power of two with symmetric nearest-integer rounding.
/// </summary>
/// <param name="value">The signed value.</param>
/// <param name="n">The base-two divisor exponent.</param>
/// <returns>The signed rounded quotient.</returns>
public static int RoundPowerOf2Signed(int value, int n)
=> (value < 0) ? -RoundPowerOf2(-value, n) : RoundPowerOf2(value, n);
/// <summary>
/// Right-shifts a long intermediate with nearest-integer rounding.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="bit">The positive shift count.</param>
/// <returns>The rounded signed result.</returns>
public static int RoundShift(long value, int bit)
{
DebugGuard.MustBeGreaterThanOrEqualTo(bit, 1, nameof(bit));
return (int)((value + (1L << (bit - 1))) >> bit);
}
/// <summary>
/// Evaluates logical implication from one Boolean condition to another.
/// </summary>
/// <param name="a">The antecedent.</param>
/// <param name="b">The consequent.</param>
/// <returns><see langword="false"/> only when <paramref name="a"/> is true and <paramref name="b"/> is false.</returns>
public static bool Implies(bool a, bool b) => !a || b;
/// <summary>
/// Gets one bit from an integer value.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="n">The zero-based bit position.</param>
/// <returns>Zero or one.</returns>
public static int GetBit(int value, int n)
=> (value & (1 << n)) >> n;
/// <summary>
/// Sets one bit in an integer value.
/// </summary>
/// <param name="endOfBlockExtra">The value to update.</param>
/// <param name="n">The zero-based bit position.</param>
public static void SetBit(ref int endOfBlockExtra, int n)
=> endOfBlockExtra |= 1 << n;
/// <summary>
/// Gets the absolute difference between two integers.
/// </summary>
/// <param name="a">The first value.</param>
/// <param name="b">The second value.</param>
/// <returns>The nonnegative absolute difference.</returns>
public static int AbsoluteDifference(int a, int b) => (a > b) ? a - b : b - a;
}

16
src/ImageSharp/Formats/Heif/Av1/Av1OperatingPointSelector.cs

@ -0,0 +1,16 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Identifies the AV1 sequence-header operating point selected by an AVIF image item.
/// </summary>
/// <param name="index">The zero-based operating-point index.</param>
internal readonly struct Av1OperatingPointSelector(byte index)
{
/// <summary>
/// Gets the zero-based operating-point index.
/// </summary>
public byte Index { get; } = index;
}

153
src/ImageSharp/Formats/Heif/Av1/Av1PartitionType.cs

@ -0,0 +1,153 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Identifies the subdivision pattern applied to an AV1 coding block.
/// </summary>
internal enum Av1PartitionType
{
/// <summary>
/// Not partitioned any further.
/// </summary>
/// <remarks>
/// <code>
/// ***
/// * *
/// ***
/// </code>
/// </remarks>
None = 0,
/// <summary>
/// Horizontally split in 2 partitions.
/// </summary>
/// <remarks>
/// <code>
/// ***
/// * *
/// ***
/// * *
/// ***
/// </code>
/// </remarks>
Horizontal = 1,
/// <summary>
/// Vertically split in 2 partitions.
/// </summary>
/// <remarks>
/// <code>
/// *****
/// * * *
/// *****
/// </code>
/// </remarks>
Vertical = 2,
/// <summary>
/// 4 equally sized partitions.
/// </summary>
/// <remarks>
/// <code>
/// *****
/// * * *
/// *****
/// * * *
/// *****
/// </code>
/// </remarks>
Split = 3,
/// <summary>
/// Horizontal split and the top partition is split again.
/// </summary>
/// <remarks>
/// <code>
/// *****
/// * * *
/// *****
/// * *
/// *****
/// </code>
/// </remarks>
HorizontalA = 4,
/// <summary>
/// Horizontal split and the bottom partition is split again.
/// </summary>
/// <remarks>
/// <code>
/// *****
/// * *
/// *****
/// * * *
/// *****
/// </code>
/// </remarks>
HorizontalB = 5,
/// <summary>
/// Vertical split and the left partition is split again.
/// </summary>
/// <remarks>
/// <code>
/// *****
/// * * *
/// *** *
/// * * *
/// *****
/// </code>
/// </remarks>
VerticalA = 6,
/// <summary>
/// Vertical split and the right partition is split again.
/// </summary>
/// <remarks>
/// <code>
/// *****
/// * * *
/// * ***
/// * * *
/// *****
/// </code>
/// </remarks>
VerticalB = 7,
/// <summary>
/// 4:1 horizontal partition.
/// </summary>
/// <remarks>
/// <code>
/// ***
/// * *
/// ***
/// * *
/// ***
/// * *
/// ***
/// * *
/// ***
/// </code>
/// </remarks>
Horizontal4 = 8,
/// <summary>
/// 4:1 vertical partition.
/// </summary>
/// <remarks>
/// <code>
/// *********
/// * * * * *
/// *********
/// </code>
/// </remarks>
Vertical4 = 9,
/// <summary>
/// Invalid value.
/// </summary>
Invalid = 255
}

116
src/ImageSharp/Formats/Heif/Av1/Av1PartitionTypeExtensions.cs

@ -0,0 +1,116 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Provides child-block geometry for AV1 partition types.
/// </summary>
internal static class Av1PartitionTypeExtensions
{
/// <summary>
/// Maps each partition type and parent block size to the size of its component blocks.
/// </summary>
private static readonly Av1BlockSize[][] PartitionSubSize = [
[
Av1BlockSize.Block4x4,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x8,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x32,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x64,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block128x128,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid
], [
Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x4,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x8,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x32,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block128x64,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid
], [
Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block4x8,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x32,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x64,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x128,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid
], [
Av1BlockSize.Block4x4,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block4x4,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x8,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x32,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x64,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid
], [
Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x4,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x8,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x32,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block128x64,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid
], [
Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x4,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x8,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x32,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block128x64,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid
], [
Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block4x8,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x32,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x64,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x128,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid
], [
Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block4x8,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x32,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x64,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x128,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid
], [
Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x4,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x8,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
], [
Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block4x16,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x32,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x64,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid,
]
];
/// <summary>
/// Gets the component block size produced by a partition operation.
/// </summary>
/// <param name="partition">The partition operation.</param>
/// <param name="blockSize">The parent block size.</param>
/// <returns>The component block size, or <see cref="Av1BlockSize.Invalid"/> when the partition is not permitted.</returns>
public static Av1BlockSize GetBlockSubSize(this Av1PartitionType partition, Av1BlockSize blockSize)
=> PartitionSubSize[(int)partition][(int)blockSize];
}

25
src/ImageSharp/Formats/Heif/Av1/Av1Plane.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Identifies an AV1 luma or chroma sample plane.
/// </summary>
internal enum Av1Plane : int
{
/// <summary>
/// The luma plane.
/// </summary>
Y = 0,
/// <summary>
/// The first chroma plane.
/// </summary>
U = 1,
/// <summary>
/// The second chroma plane.
/// </summary>
V = 2,
}

133
src/ImageSharp/Formats/Heif/Av1/Color/Av1PlanarSampleBuffer.cs

@ -0,0 +1,133 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Components;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Color;
/// <summary>
/// Adapts reconstructed AV1 planes to the shared HEIF planar color pipeline.
/// </summary>
/// <typeparam name="TSample">The native unsigned sample storage type.</typeparam>
internal readonly struct Av1PlanarSampleBuffer<TSample> : IHeifPlanarSampleBuffer<TSample>
where TSample : unmanaged
{
/// <summary>
/// The reconstructed AV1 frame containing the component planes.
/// </summary>
private readonly Av1FrameBuffer<byte> frameBuffer;
/// <summary>
/// The visible luma plane in byte-backed storage.
/// </summary>
private readonly Buffer2DRegion<byte> luma;
/// <summary>
/// The visible blue-difference plane in byte-backed storage.
/// </summary>
private readonly Buffer2DRegion<byte> chromaBlue;
/// <summary>
/// The visible red-difference plane in byte-backed storage.
/// </summary>
private readonly Buffer2DRegion<byte> chromaRed;
/// <summary>
/// Initializes a new instance of the <see cref="Av1PlanarSampleBuffer{TSample}"/> struct.
/// </summary>
/// <param name="frameBuffer">The reconstructed AV1 frame.</param>
public Av1PlanarSampleBuffer(Av1FrameBuffer<byte> frameBuffer)
{
this.frameBuffer = frameBuffer;
this.luma = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0);
this.chromaBlue = this.IsMonochrome
? default
: frameBuffer.DeriveBlockPointer(Av1Plane.U, this.ChromaSubsamplingX, this.ChromaSubsamplingY);
this.chromaRed = this.IsMonochrome
? default
: frameBuffer.DeriveBlockPointer(Av1Plane.V, this.ChromaSubsamplingX, this.ChromaSubsamplingY);
}
/// <inheritdoc/>
public readonly int Width => this.frameBuffer.Width;
/// <inheritdoc/>
public readonly int Height => this.frameBuffer.Height;
/// <inheritdoc/>
public readonly int LumaBitDepth => this.frameBuffer.BitDepth.GetBitCount();
/// <inheritdoc/>
public readonly int ChromaBitDepth => this.frameBuffer.BitDepth.GetBitCount();
/// <inheritdoc/>
public readonly bool IsMonochrome => this.frameBuffer.ColorFormat == Av1ColorFormat.Yuv400;
/// <inheritdoc/>
public readonly int ChromaSubsamplingX => this.frameBuffer.ColorConfig.SubSamplingX ? 1 : 0;
/// <inheritdoc/>
public readonly int ChromaSubsamplingY => this.frameBuffer.ColorConfig.SubSamplingY ? 1 : 0;
/// <inheritdoc/>
public readonly int ChromaPositionX
{
get
{
if (this.ChromaSubsamplingX == 0)
{
return 0;
}
// AV1 4:2:2 chroma is centered horizontally. For 4:2:0, CSP_UNKNOWN is centered while the two
// explicitly positioned layouts are co-sited with the left luma sample.
bool isCentered = this.ChromaSubsamplingY == 0
|| this.frameBuffer.ColorConfig.ChromaSamplePosition == ObuChromoSamplePosition.Unknown;
return isCentered ? 1 : 0;
}
}
/// <inheritdoc/>
public readonly int ChromaPositionY
=> this.ChromaSubsamplingY != 0 && this.frameBuffer.ColorConfig.ChromaSamplePosition != ObuChromoSamplePosition.Colocated ? 1 : 0;
/// <inheritdoc/>
public Span<TSample> GetLumaRowSpan(int row)
{
if (typeof(TSample) == typeof(byte))
{
return MemoryMarshal.Cast<byte, TSample>(this.luma.DangerousGetRowSpan(row));
}
return MemoryMarshal.Cast<ushort, TSample>(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0));
}
/// <inheritdoc/>
public Span<TSample> GetChromaBlueRowSpan(int row)
{
if (typeof(TSample) == typeof(byte))
{
return MemoryMarshal.Cast<byte, TSample>(this.chromaBlue.DangerousGetRowSpan(row));
}
return MemoryMarshal.Cast<ushort, TSample>(
this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, row, this.ChromaSubsamplingX, this.ChromaSubsamplingY));
}
/// <inheritdoc/>
public Span<TSample> GetChromaRedRowSpan(int row)
{
if (typeof(TSample) == typeof(byte))
{
return MemoryMarshal.Cast<byte, TSample>(this.chromaRed.DangerousGetRowSpan(row));
}
return MemoryMarshal.Cast<ushort, TSample>(
this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, row, this.ChromaSubsamplingX, this.ChromaSubsamplingY));
}
}

778
src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBuffer.cs

@ -0,0 +1,778 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Heif.Components;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Color;
/// <summary>
/// Owns AV1 component planes scaled to an image item's presentation extent.
/// </summary>
/// <typeparam name="TSample">The native unsigned sample storage type.</typeparam>
/// <typeparam name="TBuffer">The reconstructed AV1 plane adapter.</typeparam>
internal sealed class Av1PresentationSampleBuffer<TSample, TBuffer> : IDisposable
where TSample : unmanaged
where TBuffer : struct, IHeifPlanarSampleBuffer<TSample>
{
/// <summary>
/// The allocator that owns the presentation planes and row workspace.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The complete set of owned presentation planes, or <see langword="null"/> after disposal.
/// </summary>
private PresentationPlanes? planes;
/// <summary>
/// Initializes a new instance of the <see cref="Av1PresentationSampleBuffer{TSample, TBuffer}"/> class.
/// </summary>
/// <param name="configuration">The configuration providing allocator-owned plane storage.</param>
/// <param name="source">The unscaled reconstructed AV1 planes.</param>
/// <param name="width">The presented luma width.</param>
/// <param name="height">The presented luma height.</param>
public Av1PresentationSampleBuffer(Configuration configuration, TBuffer source, int width, int height)
{
this.memoryAllocator = configuration.MemoryAllocator;
this.Width = width;
this.Height = height;
this.LumaBitDepth = source.LumaBitDepth;
this.ChromaBitDepth = source.ChromaBitDepth;
this.IsMonochrome = source.IsMonochrome;
this.ChromaSubsamplingX = source.ChromaSubsamplingX;
this.ChromaSubsamplingY = source.ChromaSubsamplingY;
this.ChromaPositionX = source.ChromaPositionX;
this.ChromaPositionY = source.ChromaPositionY;
int sourceChromaWidth = DivideCeiling(source.Width, 1 << source.ChromaSubsamplingX);
int sourceChromaHeight = DivideCeiling(source.Height, 1 << source.ChromaSubsamplingY);
int destinationChromaWidth = DivideCeiling(width, 1 << source.ChromaSubsamplingX);
int destinationChromaHeight = DivideCeiling(height, 1 << source.ChromaSubsamplingY);
Buffer2D<TSample>? luma = null;
Buffer2D<TSample>? chromaBlue = null;
Buffer2D<TSample>? chromaRed = null;
try
{
luma = this.memoryAllocator.Allocate2D<TSample>(width, height);
this.ScalePlane(source, Av1Plane.Y, source.Width, source.Height, luma);
ChromaPlanes? chroma = null;
if (!source.IsMonochrome)
{
chromaBlue = this.memoryAllocator.Allocate2D<TSample>(destinationChromaWidth, destinationChromaHeight);
this.ScalePlane(source, Av1Plane.U, sourceChromaWidth, sourceChromaHeight, chromaBlue);
chromaRed = this.memoryAllocator.Allocate2D<TSample>(destinationChromaWidth, destinationChromaHeight);
this.ScalePlane(source, Av1Plane.V, sourceChromaWidth, sourceChromaHeight, chromaRed);
chroma = new ChromaPlanes(chromaBlue, chromaRed);
}
// Publish ownership only after every required plane has been allocated and initialized.
this.planes = new PresentationPlanes(luma, chroma);
}
catch
{
luma?.Dispose();
chromaBlue?.Dispose();
chromaRed?.Dispose();
throw;
}
}
/// <summary>
/// Gets the presented luma width.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the presented luma height.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets the luma sample bit depth.
/// </summary>
public int LumaBitDepth { get; }
/// <summary>
/// Gets the chroma sample bit depth.
/// </summary>
public int ChromaBitDepth { get; }
/// <summary>
/// Gets a value indicating whether only luma is present.
/// </summary>
public bool IsMonochrome { get; }
/// <summary>
/// Gets the horizontal chroma-subsampling shift.
/// </summary>
public int ChromaSubsamplingX { get; }
/// <summary>
/// Gets the vertical chroma-subsampling shift.
/// </summary>
public int ChromaSubsamplingY { get; }
/// <summary>
/// Gets the horizontal chroma position in half-luma-sample units.
/// </summary>
public int ChromaPositionX { get; }
/// <summary>
/// Gets the vertical chroma position in half-luma-sample units.
/// </summary>
public int ChromaPositionY { get; }
/// <summary>
/// Gets a borrowed adapter over the scaled planes.
/// </summary>
public Av1PresentationSampleBufferView<TSample, TBuffer> View => new(this);
/// <summary>
/// Releases the scaled planes.
/// </summary>
public void Dispose()
{
PresentationPlanes? planes = this.planes;
this.planes = null;
if (planes is null)
{
return;
}
planes.Value.Luma.Dispose();
ChromaPlanes? chroma = planes.Value.Chroma;
if (chroma is not null)
{
chroma.Value.Blue.Dispose();
chroma.Value.Red.Dispose();
}
}
/// <summary>
/// Gets one scaled component row.
/// </summary>
/// <param name="plane">The requested component plane.</param>
/// <param name="row">The zero-based plane row.</param>
/// <returns>The visible samples in the requested row.</returns>
public Span<TSample> GetRowSpan(Av1Plane plane, int row)
{
PresentationPlanes planes = this.planes
?? throw new ObjectDisposedException(nameof(Av1PresentationSampleBuffer<TSample, TBuffer>));
Buffer2D<TSample> buffer = plane switch
{
Av1Plane.Y => planes.Luma,
Av1Plane.U => planes.Chroma?.Blue
?? throw new InvalidOperationException("The AV1 presentation buffer has no blue-difference plane."),
_ => planes.Chroma?.Red
?? throw new InvalidOperationException("The AV1 presentation buffer has no red-difference plane.")
};
return buffer.DangerousGetRowSpan(row);
}
/// <summary>
/// Scales one component plane with the native integer filter used by pinned libavif's libyuv backend.
/// </summary>
/// <param name="source">The reconstructed component planes.</param>
/// <param name="plane">The component plane to scale.</param>
/// <param name="sourceWidth">The source plane width.</param>
/// <param name="sourceHeight">The source plane height.</param>
/// <param name="destination">The scaled destination plane.</param>
private void ScalePlane(
TBuffer source,
Av1Plane plane,
int sourceWidth,
int sourceHeight,
Buffer2D<TSample> destination)
{
int destinationWidth = destination.Width;
int destinationHeight = destination.Height;
if (sourceWidth == destinationWidth && sourceHeight == destinationHeight)
{
for (int y = 0; y < sourceHeight; y++)
{
GetSourceRow(source, plane, y)[..sourceWidth].CopyTo(destination.DangerousGetRowSpan(y));
}
return;
}
bool doublesWidth = (destinationWidth + 1) / 2 == sourceWidth;
bool doublesHeight = (destinationHeight + 1) / 2 == sourceHeight;
if (doublesWidth && doublesHeight)
{
ScaleUp2(source, plane, sourceWidth, sourceHeight, destination);
return;
}
if (doublesWidth && sourceHeight == destinationHeight)
{
for (int y = 0; y < sourceHeight; y++)
{
ScaleRowUp2Linear(
GetSourceRow(source, plane, y)[..sourceWidth],
destination.DangerousGetRowSpan(y));
}
return;
}
if (sourceHeight == destinationHeight)
{
int rowHorizontalStep = sourceWidth > 1 && destinationWidth > 1
? FixedDivideOne(sourceWidth, destinationWidth)
: 0;
for (int y = 0; y < sourceHeight; y++)
{
ScaleHorizontal(
GetSourceRow(source, plane, y)[..sourceWidth],
destination.DangerousGetRowSpan(y),
rowHorizontalStep);
}
return;
}
// Layer selection presents a lower spatial layer at the full item extent, so both dimensions are monotonic.
// The general libyuv path maps destination centers in 16.16 fixed point and retains only two horizontally
// filtered rows. This avoids a second full-plane intermediate and remains group-safe under small allocators.
using Buffer2D<TSample> horizontalRows = this.memoryAllocator.Allocate2D<TSample>(destinationWidth, 2);
int horizontalStep = sourceWidth > 1 && destinationWidth > 1
? FixedDivideOne(sourceWidth, destinationWidth)
: 0;
int verticalStep = sourceHeight > 1 && destinationHeight > 1
? FixedDivideOne(sourceHeight, destinationHeight)
: 0;
int sourcePositionY = 0;
int firstSourceRow = -1;
int secondSourceRow = -1;
int firstSlot = 0;
int secondSlot = 1;
for (int y = 0; y < destinationHeight; y++)
{
int sourceRow = sourcePositionY >> 16;
int nextSourceRow = Math.Min(sourceRow + 1, sourceHeight - 1);
if (sourceRow == secondSourceRow)
{
(firstSourceRow, secondSourceRow) = (secondSourceRow, firstSourceRow);
(firstSlot, secondSlot) = (secondSlot, firstSlot);
}
if (firstSourceRow != sourceRow)
{
ScaleHorizontal(
GetSourceRow(source, plane, sourceRow)[..sourceWidth],
horizontalRows.DangerousGetRowSpan(firstSlot),
horizontalStep);
firstSourceRow = sourceRow;
}
if (secondSourceRow != nextSourceRow)
{
ScaleHorizontal(
GetSourceRow(source, plane, nextSourceRow)[..sourceWidth],
horizontalRows.DangerousGetRowSpan(secondSlot),
horizontalStep);
secondSourceRow = nextSourceRow;
}
int verticalFraction = (sourcePositionY >> 8) & 255;
InterpolateRows(
horizontalRows.DangerousGetRowSpan(firstSlot),
horizontalRows.DangerousGetRowSpan(secondSlot),
destination.DangerousGetRowSpan(y),
verticalFraction);
sourcePositionY += verticalStep;
}
}
/// <summary>
/// Applies libyuv's edge-aware two-times bilinear kernel to one complete plane.
/// </summary>
/// <param name="source">The reconstructed component planes.</param>
/// <param name="plane">The component plane to scale.</param>
/// <param name="sourceWidth">The source plane width.</param>
/// <param name="sourceHeight">The source plane height.</param>
/// <param name="destination">The scaled destination plane.</param>
private static void ScaleUp2(
TBuffer source,
Av1Plane plane,
int sourceWidth,
int sourceHeight,
Buffer2D<TSample> destination)
{
Span<TSample> firstSource = GetSourceRow(source, plane, 0)[..sourceWidth];
Span<TSample> firstDestination = destination.DangerousGetRowSpan(0);
ScaleRowUp2Bilinear(firstSource, firstSource, firstDestination, firstDestination);
int destinationRow = 1;
for (int y = 0; y < sourceHeight - 1; y++)
{
ScaleRowUp2Bilinear(
GetSourceRow(source, plane, y)[..sourceWidth],
GetSourceRow(source, plane, y + 1)[..sourceWidth],
destination.DangerousGetRowSpan(destinationRow),
destination.DangerousGetRowSpan(destinationRow + 1));
destinationRow += 2;
}
if ((destination.Height & 1) == 0)
{
Span<TSample> lastSource = GetSourceRow(source, plane, sourceHeight - 1)[..sourceWidth];
Span<TSample> lastDestination = destination.DangerousGetRowSpan(destination.Height - 1);
ScaleRowUp2Bilinear(lastSource, lastSource, lastDestination, lastDestination);
}
}
/// <summary>
/// Gets one visible source row without boxing the codec adapter.
/// </summary>
/// <param name="source">The reconstructed component planes.</param>
/// <param name="plane">The requested component plane.</param>
/// <param name="row">The zero-based plane row.</param>
/// <returns>The source row.</returns>
private static Span<TSample> GetSourceRow(TBuffer source, Av1Plane plane, int row)
=> plane switch
{
Av1Plane.Y => source.GetLumaRowSpan(row),
Av1Plane.U => source.GetChromaBlueRowSpan(row),
_ => source.GetChromaRedRowSpan(row)
};
/// <summary>
/// Applies the edge-aware two-times bilinear row kernel.
/// </summary>
/// <param name="topSource">The upper source row.</param>
/// <param name="bottomSource">The lower source row.</param>
/// <param name="topDestination">The upper destination row.</param>
/// <param name="bottomDestination">The lower destination row.</param>
private static void ScaleRowUp2Bilinear(
ReadOnlySpan<TSample> topSource,
ReadOnlySpan<TSample> bottomSource,
Span<TSample> topDestination,
Span<TSample> bottomDestination)
{
if (typeof(TSample) == typeof(byte))
{
ScaleRowUp2BilinearByte(
MemoryMarshal.Cast<TSample, byte>(topSource),
MemoryMarshal.Cast<TSample, byte>(bottomSource),
MemoryMarshal.Cast<TSample, byte>(topDestination),
MemoryMarshal.Cast<TSample, byte>(bottomDestination));
return;
}
ScaleRowUp2BilinearUInt16(
MemoryMarshal.Cast<TSample, ushort>(topSource),
MemoryMarshal.Cast<TSample, ushort>(bottomSource),
MemoryMarshal.Cast<TSample, ushort>(topDestination),
MemoryMarshal.Cast<TSample, ushort>(bottomDestination));
}
/// <summary>
/// Applies the byte two-times bilinear row kernel through portable 128-bit lanes and a scalar tail.
/// </summary>
/// <param name="topSource">The upper source row.</param>
/// <param name="bottomSource">The lower source row.</param>
/// <param name="topDestination">The upper destination row.</param>
/// <param name="bottomDestination">The lower destination row.</param>
private static void ScaleRowUp2BilinearByte(
ReadOnlySpan<byte> topSource,
ReadOnlySpan<byte> bottomSource,
Span<byte> topDestination,
Span<byte> bottomDestination)
{
int lastSource = topSource.Length - 1;
topDestination[0] = (byte)(((3 * topSource[0]) + bottomSource[0] + 2) >> 2);
bottomDestination[0] = (byte)((topSource[0] + (3 * bottomSource[0]) + 2) >> 2);
int x = 0;
if (Vector128.IsHardwareAccelerated)
{
ref byte topSourceBase = ref MemoryMarshal.GetReference(topSource);
ref byte bottomSourceBase = ref MemoryMarshal.GetReference(bottomSource);
ref byte topDestinationBase = ref MemoryMarshal.GetReference(topDestination);
ref byte bottomDestinationBase = ref MemoryMarshal.GetReference(bottomDestination);
for (; x + 8 <= lastSource; x += 8)
{
Vector128<ushort> top0 = LoadEightBytes(ref topSourceBase, x);
Vector128<ushort> top1 = LoadEightBytes(ref topSourceBase, x + 1);
Vector128<ushort> bottom0 = LoadEightBytes(ref bottomSourceBase, x);
Vector128<ushort> bottom1 = LoadEightBytes(ref bottomSourceBase, x + 1);
CalculateBilinearPairs(
top0,
top1,
bottom0,
bottom1,
out Vector128<ushort> upperEven,
out Vector128<ushort> upperOdd,
out Vector128<ushort> lowerEven,
out Vector128<ushort> lowerOdd);
StoreInterleavedBytes(upperEven, upperOdd, ref topDestinationBase, 1 + (2 * x));
StoreInterleavedBytes(lowerEven, lowerOdd, ref bottomDestinationBase, 1 + (2 * x));
}
}
for (; x < lastSource; x++)
{
int top0 = topSource[x];
int top1 = topSource[x + 1];
int bottom0 = bottomSource[x];
int bottom1 = bottomSource[x + 1];
int destination = 1 + (2 * x);
topDestination[destination] = (byte)(((9 * top0) + (3 * top1) + (3 * bottom0) + bottom1 + 8) >> 4);
topDestination[destination + 1] = (byte)(((3 * top0) + (9 * top1) + bottom0 + (3 * bottom1) + 8) >> 4);
bottomDestination[destination] = (byte)(((3 * top0) + top1 + (9 * bottom0) + (3 * bottom1) + 8) >> 4);
bottomDestination[destination + 1] = (byte)((top0 + (3 * top1) + (3 * bottom0) + (9 * bottom1) + 8) >> 4);
}
int lastDestination = topDestination.Length - 1;
topDestination[lastDestination] = (byte)(((3 * topSource[lastSource]) + bottomSource[lastSource] + 2) >> 2);
bottomDestination[lastDestination] = (byte)((topSource[lastSource] + (3 * bottomSource[lastSource]) + 2) >> 2);
}
/// <summary>
/// Applies the unsigned 16-bit two-times bilinear row kernel through portable 128-bit lanes and a scalar tail.
/// </summary>
/// <param name="topSource">The upper source row.</param>
/// <param name="bottomSource">The lower source row.</param>
/// <param name="topDestination">The upper destination row.</param>
/// <param name="bottomDestination">The lower destination row.</param>
private static void ScaleRowUp2BilinearUInt16(
ReadOnlySpan<ushort> topSource,
ReadOnlySpan<ushort> bottomSource,
Span<ushort> topDestination,
Span<ushort> bottomDestination)
{
int lastSource = topSource.Length - 1;
topDestination[0] = (ushort)(((3 * topSource[0]) + bottomSource[0] + 2) >> 2);
bottomDestination[0] = (ushort)((topSource[0] + (3 * bottomSource[0]) + 2) >> 2);
int x = 0;
if (Vector128.IsHardwareAccelerated)
{
ref ushort topSourceBase = ref MemoryMarshal.GetReference(topSource);
ref ushort bottomSourceBase = ref MemoryMarshal.GetReference(bottomSource);
ref ushort topDestinationBase = ref MemoryMarshal.GetReference(topDestination);
ref ushort bottomDestinationBase = ref MemoryMarshal.GetReference(bottomDestination);
for (; x + Vector128<ushort>.Count <= lastSource; x += Vector128<ushort>.Count)
{
Vector128<ushort> top0 = Vector128.LoadUnsafe(ref topSourceBase, (nuint)x);
Vector128<ushort> top1 = Vector128.LoadUnsafe(ref topSourceBase, (nuint)(x + 1));
Vector128<ushort> bottom0 = Vector128.LoadUnsafe(ref bottomSourceBase, (nuint)x);
Vector128<ushort> bottom1 = Vector128.LoadUnsafe(ref bottomSourceBase, (nuint)(x + 1));
CalculateBilinearPairs(
top0,
top1,
bottom0,
bottom1,
out Vector128<ushort> upperEven,
out Vector128<ushort> upperOdd,
out Vector128<ushort> lowerEven,
out Vector128<ushort> lowerOdd);
StoreInterleavedUInt16(upperEven, upperOdd, ref topDestinationBase, 1 + (2 * x));
StoreInterleavedUInt16(lowerEven, lowerOdd, ref bottomDestinationBase, 1 + (2 * x));
}
}
for (; x < lastSource; x++)
{
int top0 = topSource[x];
int top1 = topSource[x + 1];
int bottom0 = bottomSource[x];
int bottom1 = bottomSource[x + 1];
int destination = 1 + (2 * x);
topDestination[destination] = (ushort)(((9 * top0) + (3 * top1) + (3 * bottom0) + bottom1 + 8) >> 4);
topDestination[destination + 1] = (ushort)(((3 * top0) + (9 * top1) + bottom0 + (3 * bottom1) + 8) >> 4);
bottomDestination[destination] = (ushort)(((3 * top0) + top1 + (9 * bottom0) + (3 * bottom1) + 8) >> 4);
bottomDestination[destination + 1] = (ushort)((top0 + (3 * top1) + (3 * bottom0) + (9 * bottom1) + 8) >> 4);
}
int lastDestination = topDestination.Length - 1;
topDestination[lastDestination] = (ushort)(((3 * topSource[lastSource]) + bottomSource[lastSource] + 2) >> 2);
bottomDestination[lastDestination] = (ushort)((topSource[lastSource] + (3 * bottomSource[lastSource]) + 2) >> 2);
}
/// <summary>
/// Calculates the four interleaved bilinear products for eight source positions.
/// </summary>
/// <param name="top0">The upper-left samples.</param>
/// <param name="top1">The upper-right samples.</param>
/// <param name="bottom0">The lower-left samples.</param>
/// <param name="bottom1">The lower-right samples.</param>
/// <param name="upperEven">Receives the upper left-biased samples.</param>
/// <param name="upperOdd">Receives the upper right-biased samples.</param>
/// <param name="lowerEven">Receives the lower left-biased samples.</param>
/// <param name="lowerOdd">Receives the lower right-biased samples.</param>
private static void CalculateBilinearPairs(
Vector128<ushort> top0,
Vector128<ushort> top1,
Vector128<ushort> bottom0,
Vector128<ushort> bottom1,
out Vector128<ushort> upperEven,
out Vector128<ushort> upperOdd,
out Vector128<ushort> lowerEven,
out Vector128<ushort> lowerOdd)
{
Vector128<ushort> rounding = Vector128.Create((ushort)8);
// The largest twelve-bit weighted sum is 16 * 4095 + 8, which remains within unsigned 16-bit lanes.
// Keeping eight independent source positions per vector therefore avoids widening and preserves libyuv's
// exact add-before-shift rounding for both byte and high-bit-depth presentation planes.
upperEven = (((top0 << 3) + top0) + ((top1 << 1) + top1) + ((bottom0 << 1) + bottom0) + bottom1 + rounding) >> 4;
upperOdd = (((top0 << 1) + top0) + ((top1 << 3) + top1) + bottom0 + ((bottom1 << 1) + bottom1) + rounding) >> 4;
lowerEven = (((top0 << 1) + top0) + top1 + ((bottom0 << 3) + bottom0) + ((bottom1 << 1) + bottom1) + rounding) >> 4;
lowerOdd = (top0 + ((top1 << 1) + top1) + ((bottom0 << 1) + bottom0) + ((bottom1 << 3) + bottom1) + rounding) >> 4;
}
/// <summary>
/// Loads eight byte samples as unsigned 16-bit lanes.
/// </summary>
/// <param name="source">The first source byte.</param>
/// <param name="offset">The byte offset.</param>
/// <returns>The widened samples.</returns>
private static Vector128<ushort> LoadEightBytes(ref byte source, int offset)
{
ulong packed = Unsafe.ReadUnaligned<ulong>(ref Unsafe.Add(ref source, offset));
return Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte());
}
/// <summary>
/// Interleaves and stores eight pairs of byte results.
/// </summary>
/// <param name="even">The left-biased results.</param>
/// <param name="odd">The right-biased results.</param>
/// <param name="destination">The first destination byte.</param>
/// <param name="offset">The destination byte offset.</param>
private static void StoreInterleavedBytes(
Vector128<ushort> even,
Vector128<ushort> odd,
ref byte destination,
int offset)
{
Vector128<ushort> lower = Vector128_.UnpackLow(even.AsInt16(), odd.AsInt16()).AsUInt16();
Vector128<ushort> upper = Vector128_.UnpackHigh(even.AsInt16(), odd.AsInt16()).AsUInt16();
Vector128.Narrow(lower, upper).StoreUnsafe(ref destination, (nuint)offset);
}
/// <summary>
/// Interleaves and stores eight pairs of unsigned 16-bit results.
/// </summary>
/// <param name="even">The left-biased results.</param>
/// <param name="odd">The right-biased results.</param>
/// <param name="destination">The first destination sample.</param>
/// <param name="offset">The destination sample offset.</param>
private static void StoreInterleavedUInt16(
Vector128<ushort> even,
Vector128<ushort> odd,
ref ushort destination,
int offset)
{
Vector128_.UnpackLow(even.AsInt16(), odd.AsInt16()).AsUInt16().StoreUnsafe(ref destination, (nuint)offset);
Vector128_.UnpackHigh(even.AsInt16(), odd.AsInt16()).AsUInt16().StoreUnsafe(
ref destination,
(nuint)(offset + Vector128<ushort>.Count));
}
/// <summary>
/// Applies libyuv's edge-aware horizontal two-times linear kernel.
/// </summary>
/// <param name="source">The source row.</param>
/// <param name="destination">The destination row.</param>
private static void ScaleRowUp2Linear(ReadOnlySpan<TSample> source, Span<TSample> destination)
{
if (typeof(TSample) == typeof(byte))
{
ScaleRowUp2LinearByte(
MemoryMarshal.Cast<TSample, byte>(source),
MemoryMarshal.Cast<TSample, byte>(destination));
return;
}
ScaleRowUp2LinearUInt16(
MemoryMarshal.Cast<TSample, ushort>(source),
MemoryMarshal.Cast<TSample, ushort>(destination));
}
/// <summary>
/// Applies the byte horizontal two-times linear kernel.
/// </summary>
/// <param name="source">The source row.</param>
/// <param name="destination">The destination row.</param>
private static void ScaleRowUp2LinearByte(ReadOnlySpan<byte> source, Span<byte> destination)
{
destination[0] = source[0];
for (int x = 0; x < source.Length - 1; x++)
{
int destinationX = 1 + (2 * x);
destination[destinationX] = (byte)(((3 * source[x]) + source[x + 1] + 2) >> 2);
destination[destinationX + 1] = (byte)((source[x] + (3 * source[x + 1]) + 2) >> 2);
}
destination[^1] = source[^1];
}
/// <summary>
/// Applies the unsigned 16-bit horizontal two-times linear kernel.
/// </summary>
/// <param name="source">The source row.</param>
/// <param name="destination">The destination row.</param>
private static void ScaleRowUp2LinearUInt16(ReadOnlySpan<ushort> source, Span<ushort> destination)
{
destination[0] = source[0];
for (int x = 0; x < source.Length - 1; x++)
{
int destinationX = 1 + (2 * x);
destination[destinationX] = (ushort)(((3 * source[x]) + source[x + 1] + 2) >> 2);
destination[destinationX + 1] = (ushort)((source[x] + (3 * source[x + 1]) + 2) >> 2);
}
destination[^1] = source[^1];
}
/// <summary>
/// Horizontally maps one source row with libyuv's 16.16 fixed-point bilinear positions.
/// </summary>
/// <param name="source">The source row.</param>
/// <param name="destination">The destination row.</param>
/// <param name="step">The 16.16 source-position increment.</param>
private static void ScaleHorizontal(ReadOnlySpan<TSample> source, Span<TSample> destination, int step)
{
if (source.Length == destination.Length)
{
source.CopyTo(destination);
return;
}
if (source.Length == 1)
{
destination.Fill(source[0]);
return;
}
int sourcePosition = 0;
if (typeof(TSample) == typeof(byte))
{
ReadOnlySpan<byte> sourceBytes = MemoryMarshal.Cast<TSample, byte>(source);
Span<byte> destinationBytes = MemoryMarshal.Cast<TSample, byte>(destination);
for (int x = 0; x < destinationBytes.Length; x++)
{
int sourceX = sourcePosition >> 16;
int fraction = (sourcePosition & 0xFFFF) >> 9;
int left = sourceBytes[sourceX];
int right = sourceBytes[sourceX + 1];
destinationBytes[x] = (byte)(left + (((fraction * (right - left)) + 0x40) >> 7));
sourcePosition += step;
}
return;
}
ReadOnlySpan<ushort> sourceWords = MemoryMarshal.Cast<TSample, ushort>(source);
Span<ushort> destinationWords = MemoryMarshal.Cast<TSample, ushort>(destination);
for (int x = 0; x < destinationWords.Length; x++)
{
int sourceX = sourcePosition >> 16;
int fraction = sourcePosition & 0xFFFF;
int left = sourceWords[sourceX];
int right = sourceWords[sourceX + 1];
destinationWords[x] = (ushort)(left + ((((long)fraction * (right - left)) + 0x8000) >> 16));
sourcePosition += step;
}
}
/// <summary>
/// Vertically interpolates two horizontally scaled rows.
/// </summary>
/// <param name="top">The upper row.</param>
/// <param name="bottom">The lower row.</param>
/// <param name="destination">The destination row.</param>
/// <param name="bottomWeight">The lower-row weight with a denominator of 256.</param>
private static void InterpolateRows(
ReadOnlySpan<TSample> top,
ReadOnlySpan<TSample> bottom,
Span<TSample> destination,
int bottomWeight)
{
if (bottomWeight == 0)
{
top.CopyTo(destination);
return;
}
int topWeight = 256 - bottomWeight;
if (typeof(TSample) == typeof(byte))
{
ReadOnlySpan<byte> topBytes = MemoryMarshal.Cast<TSample, byte>(top);
ReadOnlySpan<byte> bottomBytes = MemoryMarshal.Cast<TSample, byte>(bottom);
Span<byte> destinationBytes = MemoryMarshal.Cast<TSample, byte>(destination);
for (int x = 0; x < destinationBytes.Length; x++)
{
destinationBytes[x] = (byte)(((topBytes[x] * topWeight) + (bottomBytes[x] * bottomWeight) + 128) >> 8);
}
return;
}
ReadOnlySpan<ushort> topWords = MemoryMarshal.Cast<TSample, ushort>(top);
ReadOnlySpan<ushort> bottomWords = MemoryMarshal.Cast<TSample, ushort>(bottom);
Span<ushort> destinationWords = MemoryMarshal.Cast<TSample, ushort>(destination);
for (int x = 0; x < destinationWords.Length; x++)
{
destinationWords[x] = (ushort)(((topWords[x] * topWeight) + (bottomWords[x] * bottomWeight) + 128) >> 8);
}
}
/// <summary>
/// Divides two decremented lengths into libyuv's 16.16 endpoint-preserving step.
/// </summary>
/// <param name="sourceLength">The source length.</param>
/// <param name="destinationLength">The destination length.</param>
/// <returns>The 16.16 source-position increment.</returns>
private static int FixedDivideOne(int sourceLength, int destinationLength)
=> (int)((((long)sourceLength << 16) - 0x00010001) / (destinationLength - 1));
/// <summary>
/// Divides a positive value by a positive divisor with ceiling rounding.
/// </summary>
/// <param name="value">The value to divide.</param>
/// <param name="divisor">The positive divisor.</param>
/// <returns>The ceiling-rounded quotient.</returns>
private static int DivideCeiling(int value, int divisor) => (value + divisor - 1) / divisor;
private readonly struct ChromaPlanes(Buffer2D<TSample> blue, Buffer2D<TSample> red)
{
public Buffer2D<TSample> Blue { get; } = blue;
public Buffer2D<TSample> Red { get; } = red;
}
private readonly struct PresentationPlanes(Buffer2D<TSample> luma, ChromaPlanes? chroma)
{
public Buffer2D<TSample> Luma { get; } = luma;
public ChromaPlanes? Chroma { get; } = chroma;
}
}

63
src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBufferView.cs

@ -0,0 +1,63 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Components;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Color;
/// <summary>
/// Exposes borrowed rows from an owned AV1 presentation buffer to the shared HEIF color converter.
/// </summary>
/// <typeparam name="TSample">The native unsigned sample storage type.</typeparam>
/// <typeparam name="TBuffer">The reconstructed AV1 plane adapter owned by the presentation buffer.</typeparam>
internal readonly struct Av1PresentationSampleBufferView<TSample, TBuffer> : IHeifPlanarSampleBuffer<TSample>
where TSample : unmanaged
where TBuffer : struct, IHeifPlanarSampleBuffer<TSample>
{
/// <summary>
/// The owner that keeps all exposed rows alive.
/// </summary>
private readonly Av1PresentationSampleBuffer<TSample, TBuffer> owner;
/// <summary>
/// Initializes a new instance of the <see cref="Av1PresentationSampleBufferView{TSample, TBuffer}"/> struct.
/// </summary>
/// <param name="owner">The scaled plane owner.</param>
public Av1PresentationSampleBufferView(Av1PresentationSampleBuffer<TSample, TBuffer> owner) => this.owner = owner;
/// <inheritdoc/>
public int Width => this.owner.Width;
/// <inheritdoc/>
public int Height => this.owner.Height;
/// <inheritdoc/>
public int LumaBitDepth => this.owner.LumaBitDepth;
/// <inheritdoc/>
public int ChromaBitDepth => this.owner.ChromaBitDepth;
/// <inheritdoc/>
public bool IsMonochrome => this.owner.IsMonochrome;
/// <inheritdoc/>
public int ChromaSubsamplingX => this.owner.ChromaSubsamplingX;
/// <inheritdoc/>
public int ChromaSubsamplingY => this.owner.ChromaSubsamplingY;
/// <inheritdoc/>
public int ChromaPositionX => this.owner.ChromaPositionX;
/// <inheritdoc/>
public int ChromaPositionY => this.owner.ChromaPositionY;
/// <inheritdoc/>
public Span<TSample> GetLumaRowSpan(int row) => this.owner.GetRowSpan(Av1Plane.Y, row);
/// <inheritdoc/>
public Span<TSample> GetChromaBlueRowSpan(int row) => this.owner.GetRowSpan(Av1Plane.U, row);
/// <inheritdoc/>
public Span<TSample> GetChromaRedRowSpan(int row) => this.owner.GetRowSpan(Av1Plane.V, row);
}

203
src/ImageSharp/Formats/Heif/Av1/Color/Av1YuvConverter.cs

@ -0,0 +1,203 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Components;
using SixLabors.ImageSharp.Formats.Heif.Components.Alpha;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Color;
/// <summary>
/// Adapts AV1 color signaling and reconstructed planes to the shared HEIF color pipeline.
/// </summary>
internal static class Av1YuvConverter
{
/// <summary>
/// Converts the reconstructed component planes to packed pixels.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param>
/// <param name="frameBuffer">The reconstructed AV1 frame.</param>
/// <param name="image">The destination image frame.</param>
public static void ConvertToRgb<TPixel>(Configuration configuration, Av1FrameBuffer<byte> frameBuffer, ImageFrame<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer, out HeifColorConversionMode mode);
if (frameBuffer.BitDepth == Av1BitDepth.EightBit)
{
Av1PlanarSampleBuffer<byte> buffer = new(frameBuffer);
if (buffer.Width != image.Width || buffer.Height != image.Height)
{
// AVIF spatial-layer selection scales native YUV planes before color conversion. The retained
// reconstruction remains untouched because later dependent layers can still reference its coded
// dimensions, while this short-lived owner contains only the presented sample grid.
using Av1PresentationSampleBuffer<byte, Av1PlanarSampleBuffer<byte>> presentationBuffer = new(
configuration,
buffer,
image.Width,
image.Height);
HeifPlanarColorConverter.ConvertToRgb<
TPixel,
Av1PresentationSampleBufferView<byte, Av1PlanarSampleBuffer<byte>>,
byte,
HeifByteSampleConverter>(
configuration,
presentationBuffer.View,
image,
in parameters,
mode);
return;
}
HeifPlanarColorConverter.ConvertToRgb<TPixel, Av1PlanarSampleBuffer<byte>, byte, HeifByteSampleConverter>(
configuration,
buffer,
image,
in parameters,
mode);
return;
}
Av1PlanarSampleBuffer<ushort> highBitDepthBuffer = new(frameBuffer);
if (highBitDepthBuffer.Width != image.Width || highBitDepthBuffer.Height != image.Height)
{
using Av1PresentationSampleBuffer<ushort, Av1PlanarSampleBuffer<ushort>> presentationBuffer = new(
configuration,
highBitDepthBuffer,
image.Width,
image.Height);
HeifPlanarColorConverter.ConvertToRgb<
TPixel,
Av1PresentationSampleBufferView<ushort, Av1PlanarSampleBuffer<ushort>>>(
configuration,
presentationBuffer.View,
image,
in parameters,
mode);
return;
}
HeifPlanarColorConverter.ConvertToRgb<TPixel, Av1PlanarSampleBuffer<ushort>>(
configuration,
highBitDepthBuffer,
image,
in parameters,
mode);
}
/// <summary>
/// Composes the reconstructed luma plane into a packed color frame as auxiliary alpha.
/// </summary>
/// <typeparam name="TPixel">The destination color pixel type.</typeparam>
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param>
/// <param name="frameBuffer">The reconstructed AV1 frame containing the alpha luma plane.</param>
/// <param name="destination">The packed color frame receiving alpha values.</param>
/// <param name="outputSize">The complete presented size of the auxiliary image or grid tile.</param>
/// <param name="destinationRectangle">The destination region receiving the top-left portion of the presented alpha image.</param>
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
public static void ComposeAlpha<TPixel>(
Configuration configuration,
Av1FrameBuffer<byte> frameBuffer,
ImageFrame<TPixel> destination,
Size outputSize,
Rectangle destinationRectangle,
bool premultiplied)
where TPixel : unmanaged, IPixel<TPixel>
{
HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer, out _);
Rectangle sourceRectangle = new(0, 0, frameBuffer.Width, frameBuffer.Height);
if (frameBuffer.BitDepth == Av1BitDepth.EightBit)
{
Av1PlanarSampleBuffer<byte> buffer = new(frameBuffer);
HeifPlanarAlphaCompositor.Compose<TPixel, Av1PlanarSampleBuffer<byte>, byte, HeifByteSampleConverter>(
configuration,
buffer,
destination,
in parameters,
sourceRectangle,
outputSize,
destinationRectangle,
premultiplied);
return;
}
Av1PlanarSampleBuffer<ushort> highBitDepthBuffer = new(frameBuffer);
HeifPlanarAlphaCompositor.Compose<TPixel, Av1PlanarSampleBuffer<ushort>, ushort, HeifUShortSampleConverter>(
configuration,
highBitDepthBuffer,
destination,
in parameters,
sourceRectangle,
outputSize,
destinationRectangle,
premultiplied);
}
/// <summary>
/// Converts packed pixels to the configured monochrome or component planes used by the AV1 encoder.
/// </summary>
/// <typeparam name="TPixel">The source pixel type.</typeparam>
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param>
/// <param name="image">The source image frame.</param>
/// <param name="frameBuffer">The destination AV1 frame.</param>
public static void ConvertFromRgb<TPixel>(Configuration configuration, ImageFrame<TPixel> image, Av1FrameBuffer<byte> frameBuffer)
where TPixel : unmanaged, IPixel<TPixel>
{
HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer, out HeifColorConversionMode mode);
if (frameBuffer.BitDepth == Av1BitDepth.EightBit)
{
Av1PlanarSampleBuffer<byte> buffer = new(frameBuffer);
HeifPlanarColorConverter.ConvertFromRgb<TPixel, Av1PlanarSampleBuffer<byte>, byte, HeifByteSampleConverter>(
configuration,
image,
buffer,
in parameters,
mode);
return;
}
Av1PlanarSampleBuffer<ushort> highBitDepthBuffer = new(frameBuffer);
HeifPlanarColorConverter.ConvertFromRgb<TPixel, Av1PlanarSampleBuffer<ushort>, ushort, HeifUShortSampleConverter>(
configuration,
image,
highBitDepthBuffer,
in parameters,
mode);
}
/// <summary>
/// Resolves the H.273 conversion mode, matrix coefficients, and sample range for a frame.
/// </summary>
/// <param name="frameBuffer">The AV1 frame containing the signaled color configuration.</param>
/// <param name="mode">The resolved conversion mode.</param>
/// <returns>The resolved conversion parameters.</returns>
private static HeifColorConversionParameters GetConversionParameters(Av1FrameBuffer<byte> frameBuffer, out HeifColorConversionMode mode)
{
if (frameBuffer.ColorConfig.ChromaSamplePosition == ObuChromoSamplePosition.Reserved)
{
throw new InvalidImageContentException("The reserved AV1 chroma sample position is invalid.");
}
bool isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400;
return HeifColorConversionParameters.Create(
(CicpColorPrimaries)(byte)frameBuffer.ColorConfig.ColorPrimaries,
(CicpTransferCharacteristics)(byte)frameBuffer.ColorConfig.TransferCharacteristics,
(CicpMatrixCoefficients)(byte)frameBuffer.ColorConfig.MatrixCoefficients,
frameBuffer.ColorConfig.ColorRange,
frameBuffer.BitDepth.GetBitCount(),
frameBuffer.BitDepth.GetBitCount(),
isMonochrome,
frameBuffer.ColorFormat == Av1ColorFormat.Yuv444,
out mode);
}
}

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

File diff suppressed because it is too large

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

@ -0,0 +1,420 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Stores and adapts an AV1 inverse cumulative distribution used by the range coder.
/// </summary>
internal sealed class Av1Distribution
{
/// <summary>
/// The exclusive upper bound of the Q15 probability domain.
/// </summary>
public const int ProbabilityTop = 1 << ProbabilityBitCount;
/// <summary>
/// The minimum sub-range reserved for each symbol during range coding.
/// </summary>
public const int ProbabilityMinimum = 4;
/// <summary>
/// The shift that converts stored Q15 cumulative values to the range-coder precision.
/// </summary>
public const int CdfShift = 15 - ProbabilityBitCount;
/// <summary>
/// The precision reduction applied before multiplying a cumulative value by the coding range.
/// </summary>
public const int ProbabilityShift = 6;
/// <summary>
/// The number of fractional bits in a stored cumulative probability.
/// </summary>
private const int ProbabilityBitCount = 15;
/// <summary>
/// The inverse cumulative thresholds followed by the required zero sentinel.
/// </summary>
private readonly uint[] probabilities;
/// <summary>
/// The symbol-count contribution to the adaptive update rate.
/// </summary>
private readonly int speed;
/// <summary>
/// The capped number of observations already incorporated into this distribution.
/// </summary>
private int updateCount;
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a binary alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
public Av1Distribution(uint p0)
: this([p0, 0], 1)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a three-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
public Av1Distribution(uint p0, uint p1)
: this([p0, p1, 0], 1)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a four-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
public Av1Distribution(uint p0, uint p1, uint p2)
: this([p0, p1, p2, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a five-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3)
: this([p0, p1, p2, p3, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a six-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4)
: this([p0, p1, p2, p3, p4, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a seven-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5)
: this([p0, p1, p2, p3, p4, p5, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for an eight-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
/// <param name="p6">The cumulative threshold following symbol six.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6)
: this([p0, p1, p2, p3, p4, p5, p6, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a nine-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
/// <param name="p6">The cumulative threshold following symbol six.</param>
/// <param name="p7">The cumulative threshold following symbol seven.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7)
: this([p0, p1, p2, p3, p4, p5, p6, p7, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a ten-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
/// <param name="p6">The cumulative threshold following symbol six.</param>
/// <param name="p7">The cumulative threshold following symbol seven.</param>
/// <param name="p8">The cumulative threshold following symbol eight.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8)
: this([p0, p1, p2, p3, p4, p5, p6, p7, p8, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for an eleven-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
/// <param name="p6">The cumulative threshold following symbol six.</param>
/// <param name="p7">The cumulative threshold following symbol seven.</param>
/// <param name="p8">The cumulative threshold following symbol eight.</param>
/// <param name="p9">The cumulative threshold following symbol nine.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8, uint p9)
: this([p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a twelve-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
/// <param name="p6">The cumulative threshold following symbol six.</param>
/// <param name="p7">The cumulative threshold following symbol seven.</param>
/// <param name="p8">The cumulative threshold following symbol eight.</param>
/// <param name="p9">The cumulative threshold following symbol nine.</param>
/// <param name="p10">The cumulative threshold following symbol ten.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8, uint p9, uint p10)
: this([p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a thirteen-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
/// <param name="p6">The cumulative threshold following symbol six.</param>
/// <param name="p7">The cumulative threshold following symbol seven.</param>
/// <param name="p8">The cumulative threshold following symbol eight.</param>
/// <param name="p9">The cumulative threshold following symbol nine.</param>
/// <param name="p10">The cumulative threshold following symbol ten.</param>
/// <param name="p11">The cumulative threshold following symbol eleven.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8, uint p9, uint p10, uint p11)
: this([p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a fourteen-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
/// <param name="p6">The cumulative threshold following symbol six.</param>
/// <param name="p7">The cumulative threshold following symbol seven.</param>
/// <param name="p8">The cumulative threshold following symbol eight.</param>
/// <param name="p9">The cumulative threshold following symbol nine.</param>
/// <param name="p10">The cumulative threshold following symbol ten.</param>
/// <param name="p11">The cumulative threshold following symbol eleven.</param>
/// <param name="p12">The cumulative threshold following symbol twelve.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8, uint p9, uint p10, uint p11, uint p12)
: this([p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class for a sixteen-symbol alphabet.
/// </summary>
/// <param name="p0">The cumulative threshold following symbol zero.</param>
/// <param name="p1">The cumulative threshold following symbol one.</param>
/// <param name="p2">The cumulative threshold following symbol two.</param>
/// <param name="p3">The cumulative threshold following symbol three.</param>
/// <param name="p4">The cumulative threshold following symbol four.</param>
/// <param name="p5">The cumulative threshold following symbol five.</param>
/// <param name="p6">The cumulative threshold following symbol six.</param>
/// <param name="p7">The cumulative threshold following symbol seven.</param>
/// <param name="p8">The cumulative threshold following symbol eight.</param>
/// <param name="p9">The cumulative threshold following symbol nine.</param>
/// <param name="p10">The cumulative threshold following symbol ten.</param>
/// <param name="p11">The cumulative threshold following symbol eleven.</param>
/// <param name="p12">The cumulative threshold following symbol twelve.</param>
/// <param name="p13">The cumulative threshold following symbol thirteen.</param>
/// <param name="p14">The cumulative threshold following symbol fourteen.</param>
public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8, uint p9, uint p10, uint p11, uint p12, uint p13, uint p14)
: this([p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, 0], 2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class from forward cumulative thresholds.
/// </summary>
/// <param name="props">The forward Q15 thresholds followed by a zero sentinel slot.</param>
/// <param name="speed">The symbol-count contribution to the update rate.</param>
private Av1Distribution(ReadOnlySpan<uint> props, int speed)
{
this.probabilities = new uint[props.Length];
// AV1 range coding consumes inverse cumulative thresholds. The defaults are written in the more readable
// forward form, so convert every real threshold while leaving the final zero sentinel untouched.
for (int i = 0; i < props.Length - 1; i++)
{
this.probabilities[i] = ProbabilityTop - props[i];
}
this.NumberOfSymbols = props.Length;
this.speed = speed;
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1Distribution"/> class with the same probability and adaptation state as another distribution.
/// </summary>
/// <param name="source">The distribution state to copy.</param>
private Av1Distribution(Av1Distribution source)
{
this.probabilities = new uint[source.probabilities.Length];
source.probabilities.CopyTo(this.probabilities, 0);
// The adaptation rate depends on both the alphabet size and prior update count, so copying only the
// thresholds would make the cloned frame context diverge after its next symbol.
this.speed = source.speed;
this.updateCount = source.updateCount;
this.NumberOfSymbols = source.NumberOfSymbols;
}
/// <summary>
/// Gets the number of symbols represented by the distribution.
/// </summary>
public int NumberOfSymbols { get; }
/// <summary>
/// Gets an inverse cumulative threshold by symbol index.
/// </summary>
/// <param name="index">The zero-based threshold index.</param>
/// <returns>The Q15 inverse cumulative threshold.</returns>
public uint this[int index] => this.probabilities[index];
/// <summary>
/// Creates an independently adaptable copy of a distribution.
/// </summary>
/// <returns>A distribution initialized with the same probabilities and update count.</returns>
public Av1Distribution CreateCopy() => new(this);
/// <summary>
/// Replaces the probability and adaptation state with the state of another distribution having the same alphabet.
/// </summary>
/// <param name="source">The distribution state to copy.</param>
public void CopyFrom(Av1Distribution source)
{
// Entropy contexts are created from the same fixed default table shape. Copy only mutable state so resetting a
// working tile never allocates or replaces the distribution objects referenced by the symbol decoder.
source.probabilities.AsSpan().CopyTo(this.probabilities);
this.updateCount = source.updateCount;
}
/// <summary>
/// Resets the observation count that controls the adaptive update rate without changing probability thresholds.
/// </summary>
public void ResetUpdateCount() => this.updateCount = 0;
/// <summary>
/// Creates independently adaptable copies of a distribution array.
/// </summary>
/// <param name="source">The distributions to copy.</param>
/// <returns>An array with the same shape and distribution state.</returns>
public static Av1Distribution[] CreateCopy(Av1Distribution[] source)
{
Av1Distribution[] result = new Av1Distribution[source.Length];
for (int i = 0; i < source.Length; i++)
{
result[i] = source[i].CreateCopy();
}
return result;
}
/// <summary>
/// Creates independently adaptable copies of a two-dimensional jagged distribution array.
/// </summary>
/// <param name="source">The distributions to copy.</param>
/// <returns>An array with the same shape and distribution state.</returns>
public static Av1Distribution[][] CreateCopy(Av1Distribution[][] source)
{
Av1Distribution[][] result = new Av1Distribution[source.Length][];
for (int i = 0; i < source.Length; i++)
{
result[i] = CreateCopy(source[i]);
}
return result;
}
/// <summary>
/// Creates independently adaptable copies of a three-dimensional jagged distribution array.
/// </summary>
/// <param name="source">The distributions to copy.</param>
/// <returns>An array with the same shape and distribution state.</returns>
public static Av1Distribution[][][] CreateCopy(Av1Distribution[][][] source)
{
Av1Distribution[][][] result = new Av1Distribution[source.Length][][];
for (int i = 0; i < source.Length; i++)
{
result[i] = CreateCopy(source[i]);
}
return result;
}
/// <summary>
/// Adapts the cumulative thresholds after coding one symbol.
/// </summary>
/// <param name="value">The zero-based symbol that was coded.</param>
public void Update(int value)
{
// AV1 slows adaptation after 16 and 32 observations. The symbol-count term is precomputed by each overload
// because every distribution has a fixed alphabet size.
int rate15 = this.updateCount > 15 ? 1 : 0;
int rate31 = this.updateCount > 31 ? 1 : 0;
int rate = 3 + rate15 + rate31 + this.speed;
int tmp = ProbabilityTop;
// Switching tmp to zero at the observed symbol moves the thresholds on either side toward the sample while
// preserving their inverse-cumulative ordering in one pass.
for (int i = 0; i < this.NumberOfSymbols - 1; i++)
{
tmp = i == value ? 0 : tmp;
uint p = this.probabilities[i];
if (tmp < p)
{
this.probabilities[i] -= (ushort)((p - tmp) >> rate);
}
else
{
this.probabilities[i] += (ushort)((tmp - p) >> rate);
}
}
int rate32 = this.updateCount < 32 ? 1 : 0;
this.updateCount += rate32;
}
}

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

@ -0,0 +1,757 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Owns the adaptive AV1 distributions currently implemented by the frame and tile syntax decoders.
/// </summary>
/// <remarks>
/// One frame context supplies the initial state copied into every tile context. Each tile adapts an independent working
/// copy, and only the tile selected by <c>context_update_tile_id</c> supplies the completed frame snapshot.
/// </remarks>
internal sealed class Av1FrameEntropyContext
{
/// <summary>
/// The inclusive upper bound of the first AV1 coefficient-probability quantizer band.
/// </summary>
private const int FirstQuantizerBandMaximum = 20;
/// <summary>
/// The inclusive upper bound of the second AV1 coefficient-probability quantizer band.
/// </summary>
private const int SecondQuantizerBandMaximum = 60;
/// <summary>
/// The inclusive upper bound of the third AV1 coefficient-probability quantizer band.
/// </summary>
private const int ThirdQuantizerBandMaximum = 120;
/// <summary>
/// The immutable normative contexts used to restore reusable frame state without rebuilding distribution graphs.
/// </summary>
private static readonly Av1FrameEntropyContext[] DefaultPrototypes =
[
new((byte)0),
new((byte)1),
new((byte)2),
new((byte)3)
];
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameEntropyContext"/> class from the normative default
/// distributions selected by a frame quantizer index.
/// </summary>
/// <param name="qIndex">The frame base quantizer index selecting coefficient distribution defaults.</param>
public Av1FrameEntropyContext(int qIndex)
: this(DefaultPrototypes[GetQContext(qIndex)])
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameEntropyContext"/> class as an immutable normative prototype.
/// </summary>
/// <param name="qContext">The zero-based coefficient-probability quantizer band.</param>
private Av1FrameEntropyContext(byte qContext)
{
int qIndex = qContext switch
{
0 => 0,
1 => FirstQuantizerBandMaximum + 1,
2 => SecondQuantizerBandMaximum + 1,
_ => ThirdQuantizerBandMaximum + 1
};
// Every default-distribution accessor constructs independently mutable state. Retaining those returned
// graphs directly confines generated-table construction to the four process-wide quantizer-band prototypes.
this.IntraBlockCopy = Av1DefaultDistributions.IntraBlockCopy;
// Normal motion vectors and intra-block-copy displacement vectors start from identical defaults, but AV1
// adapts NMVC and NDVC independently. Distinct object graphs preserve that separation for the prototype too.
this.MotionVector = new();
this.DisplacementVector = new();
this.SwitchableRestoration = Av1DefaultDistributions.SwitchableRestoration;
this.WienerRestoration = Av1DefaultDistributions.WienerRestoration;
this.SgrProjectionRestoration = Av1DefaultDistributions.SgrProjectionRestoration;
this.PaletteYMode = Av1DefaultDistributions.PaletteYMode;
this.PaletteUvMode = Av1DefaultDistributions.PaletteUvMode;
this.PaletteYSize = Av1DefaultDistributions.PaletteYSize;
this.PaletteUvSize = Av1DefaultDistributions.PaletteUvSize;
this.PaletteYColorIndex = Av1DefaultDistributions.PaletteYColorIndex;
this.PaletteUvColorIndex = Av1DefaultDistributions.PaletteUvColorIndex;
this.PartitionTypes = Av1DefaultDistributions.PartitionTypes;
this.FrameYMode = Av1DefaultDistributions.FrameYMode;
this.KeyFrameYMode = Av1DefaultDistributions.KeyFrameYMode;
this.IntraInter = Av1DefaultDistributions.IntraInter;
this.NewMv = Av1DefaultDistributions.NewMv;
this.ZeroMv = Av1DefaultDistributions.ZeroMv;
this.RefMv = Av1DefaultDistributions.RefMv;
this.Drl = Av1DefaultDistributions.Drl;
this.SingleReference = Av1DefaultDistributions.SingleReference;
this.CompInter = Av1DefaultDistributions.CompInter;
this.CompoundReferenceType = Av1DefaultDistributions.CompoundReferenceType;
this.UnidirectionalCompoundReference = Av1DefaultDistributions.UnidirectionalCompoundReference;
this.CompoundReference = Av1DefaultDistributions.CompoundReference;
this.CompoundBackwardReference = Av1DefaultDistributions.CompoundBackwardReference;
this.InterCompoundMode = Av1DefaultDistributions.InterCompoundMode;
this.InterIntra = Av1DefaultDistributions.InterIntra;
this.InterIntraMode = Av1DefaultDistributions.InterIntraMode;
this.WedgeInterIntra = Av1DefaultDistributions.WedgeInterIntra;
this.CompoundType = Av1DefaultDistributions.CompoundType;
this.WedgeIndex = Av1DefaultDistributions.WedgeIndex;
this.CompoundIndex = Av1DefaultDistributions.CompoundIndex;
this.CompoundGroupIndex = Av1DefaultDistributions.CompoundGroupIndex;
this.MotionMode = Av1DefaultDistributions.MotionMode;
this.Obmc = Av1DefaultDistributions.Obmc;
this.SwitchableInterpolation = Av1DefaultDistributions.SwitchableInterpolation;
this.UvMode = Av1DefaultDistributions.UvMode;
this.Skip = Av1DefaultDistributions.Skip;
this.SkipMode = Av1DefaultDistributions.SkipMode;
this.DeltaLoopFilterAbsolute = Av1DefaultDistributions.DeltaLoopFilterAbsolute;
this.DeltaQuantizerAbsolute = Av1DefaultDistributions.DeltaQuantizerAbsolute;
this.SegmentId = Av1DefaultDistributions.SegmentId;
this.SegmentIdPredicted = Av1DefaultDistributions.SegmentIdPredicted;
this.AngleDelta = Av1DefaultDistributions.AngleDelta;
this.FilterIntraMode = Av1DefaultDistributions.FilterIntraMode;
this.FilterIntra = Av1DefaultDistributions.FilterIntra;
this.TransformSize = Av1DefaultDistributions.TransformSize;
this.TransformPartition = Av1DefaultDistributions.TransformPartition;
this.ChromaFromLumaSign = Av1DefaultDistributions.ChromaFromLumaSign;
this.ChromaFromLumaAlpha = Av1DefaultDistributions.ChromaFromLumaAlpha;
this.IntraExtendedTransform = Av1DefaultDistributions.IntraExtendedTransform;
this.InterExtendedTransform = Av1DefaultDistributions.InterExtendedTransform;
// Coefficient defaults use one of four quantizer bands. Their array shapes remain fixed, so later tile resets
// copy only thresholds and update counts into this context's already allocated distribution graph.
this.EndOfBlockFlag = Av1DefaultDistributions.GetEndOfBlockFlag(qIndex);
this.CoefficientsBase = Av1DefaultDistributions.GetCoefficientsBase(qIndex);
this.BaseEndOfBlock = Av1DefaultDistributions.GetBaseEndOfBlock(qIndex);
this.DcSign = Av1DefaultDistributions.GetDcSign(qIndex);
this.CoefficientsBaseRange = Av1DefaultDistributions.GetCoefficientsBaseRange(qIndex);
this.TransformBlockSkip = Av1DefaultDistributions.GetTransformBlockSkip(qIndex);
this.EndOfBlockExtra = Av1DefaultDistributions.GetEndOfBlockExtra(qIndex);
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameEntropyContext"/> class with an independently adaptable copy of a prototype.
/// </summary>
/// <param name="source">The prototype or retained context whose state is copied.</param>
private Av1FrameEntropyContext(Av1FrameEntropyContext source)
{
// Session and retained-frame contexts need one mutable graph, not four generated quantizer-band graphs whose
// unused bands are immediately discarded. Deep-copy the already selected prototype shape exactly once.
this.IntraBlockCopy = source.IntraBlockCopy.CreateCopy();
this.MotionVector = new();
this.MotionVector.CopyFrom(source.MotionVector);
this.DisplacementVector = new();
this.DisplacementVector.CopyFrom(source.DisplacementVector);
this.SwitchableRestoration = source.SwitchableRestoration.CreateCopy();
this.WienerRestoration = source.WienerRestoration.CreateCopy();
this.SgrProjectionRestoration = source.SgrProjectionRestoration.CreateCopy();
this.PaletteYMode = Av1Distribution.CreateCopy(source.PaletteYMode);
this.PaletteUvMode = Av1Distribution.CreateCopy(source.PaletteUvMode);
this.PaletteYSize = Av1Distribution.CreateCopy(source.PaletteYSize);
this.PaletteUvSize = Av1Distribution.CreateCopy(source.PaletteUvSize);
this.PaletteYColorIndex = Av1Distribution.CreateCopy(source.PaletteYColorIndex);
this.PaletteUvColorIndex = Av1Distribution.CreateCopy(source.PaletteUvColorIndex);
this.PartitionTypes = Av1Distribution.CreateCopy(source.PartitionTypes);
this.FrameYMode = Av1Distribution.CreateCopy(source.FrameYMode);
this.KeyFrameYMode = Av1Distribution.CreateCopy(source.KeyFrameYMode);
this.IntraInter = Av1Distribution.CreateCopy(source.IntraInter);
this.NewMv = Av1Distribution.CreateCopy(source.NewMv);
this.ZeroMv = Av1Distribution.CreateCopy(source.ZeroMv);
this.RefMv = Av1Distribution.CreateCopy(source.RefMv);
this.Drl = Av1Distribution.CreateCopy(source.Drl);
this.SingleReference = Av1Distribution.CreateCopy(source.SingleReference);
this.CompInter = Av1Distribution.CreateCopy(source.CompInter);
this.CompoundReferenceType = Av1Distribution.CreateCopy(source.CompoundReferenceType);
this.UnidirectionalCompoundReference = Av1Distribution.CreateCopy(source.UnidirectionalCompoundReference);
this.CompoundReference = Av1Distribution.CreateCopy(source.CompoundReference);
this.CompoundBackwardReference = Av1Distribution.CreateCopy(source.CompoundBackwardReference);
this.InterCompoundMode = Av1Distribution.CreateCopy(source.InterCompoundMode);
this.InterIntra = Av1Distribution.CreateCopy(source.InterIntra);
this.InterIntraMode = Av1Distribution.CreateCopy(source.InterIntraMode);
this.WedgeInterIntra = Av1Distribution.CreateCopy(source.WedgeInterIntra);
this.CompoundType = Av1Distribution.CreateCopy(source.CompoundType);
this.WedgeIndex = Av1Distribution.CreateCopy(source.WedgeIndex);
this.CompoundIndex = Av1Distribution.CreateCopy(source.CompoundIndex);
this.CompoundGroupIndex = Av1Distribution.CreateCopy(source.CompoundGroupIndex);
this.MotionMode = Av1Distribution.CreateCopy(source.MotionMode);
this.Obmc = Av1Distribution.CreateCopy(source.Obmc);
this.SwitchableInterpolation = Av1Distribution.CreateCopy(source.SwitchableInterpolation);
this.UvMode = Av1Distribution.CreateCopy(source.UvMode);
this.Skip = Av1Distribution.CreateCopy(source.Skip);
this.SkipMode = Av1Distribution.CreateCopy(source.SkipMode);
this.DeltaLoopFilterAbsolute = source.DeltaLoopFilterAbsolute.CreateCopy();
this.DeltaQuantizerAbsolute = source.DeltaQuantizerAbsolute.CreateCopy();
this.SegmentId = Av1Distribution.CreateCopy(source.SegmentId);
this.SegmentIdPredicted = Av1Distribution.CreateCopy(source.SegmentIdPredicted);
this.AngleDelta = Av1Distribution.CreateCopy(source.AngleDelta);
this.FilterIntraMode = source.FilterIntraMode.CreateCopy();
this.FilterIntra = Av1Distribution.CreateCopy(source.FilterIntra);
this.TransformSize = Av1Distribution.CreateCopy(source.TransformSize);
this.TransformPartition = Av1Distribution.CreateCopy(source.TransformPartition);
this.EndOfBlockFlag = Av1Distribution.CreateCopy(source.EndOfBlockFlag);
this.CoefficientsBase = Av1Distribution.CreateCopy(source.CoefficientsBase);
this.BaseEndOfBlock = Av1Distribution.CreateCopy(source.BaseEndOfBlock);
this.DcSign = Av1Distribution.CreateCopy(source.DcSign);
this.CoefficientsBaseRange = Av1Distribution.CreateCopy(source.CoefficientsBaseRange);
this.TransformBlockSkip = Av1Distribution.CreateCopy(source.TransformBlockSkip);
this.EndOfBlockExtra = Av1Distribution.CreateCopy(source.EndOfBlockExtra);
this.ChromaFromLumaSign = source.ChromaFromLumaSign.CreateCopy();
this.ChromaFromLumaAlpha = Av1Distribution.CreateCopy(source.ChromaFromLumaAlpha);
this.IntraExtendedTransform = Av1Distribution.CreateCopy(source.IntraExtendedTransform);
this.InterExtendedTransform = Av1Distribution.CreateCopy(source.InterExtendedTransform);
}
/// <summary>
/// Gets the intra-block-copy distribution.
/// </summary>
public Av1Distribution IntraBlockCopy { get; }
/// <summary>
/// Gets the entropy context used by normal inter-prediction motion vectors.
/// </summary>
public Av1MotionVectorContext MotionVector { get; }
/// <summary>
/// Gets the integer displacement-vector context used by intra-block copy.
/// </summary>
public Av1MotionVectorContext DisplacementVector { get; }
/// <summary>
/// Gets the switchable loop-restoration distribution.
/// </summary>
public Av1Distribution SwitchableRestoration { get; }
/// <summary>
/// Gets the Wiener loop-restoration distribution.
/// </summary>
public Av1Distribution WienerRestoration { get; }
/// <summary>
/// Gets the self-guided loop-restoration distribution.
/// </summary>
public Av1Distribution SgrProjectionRestoration { get; }
/// <summary>
/// Gets the luma palette-mode distributions.
/// </summary>
public Av1Distribution[][] PaletteYMode { get; }
/// <summary>
/// Gets the chroma palette-mode distributions.
/// </summary>
public Av1Distribution[] PaletteUvMode { get; }
/// <summary>
/// Gets the luma palette-size distributions.
/// </summary>
public Av1Distribution[] PaletteYSize { get; }
/// <summary>
/// Gets the chroma palette-size distributions.
/// </summary>
public Av1Distribution[] PaletteUvSize { get; }
/// <summary>
/// Gets the luma palette color-index distributions.
/// </summary>
public Av1Distribution[][] PaletteYColorIndex { get; }
/// <summary>
/// Gets the chroma palette color-index distributions.
/// </summary>
public Av1Distribution[][] PaletteUvColorIndex { get; }
/// <summary>
/// Gets the partition-type distributions.
/// </summary>
public Av1Distribution[] PartitionTypes { get; }
/// <summary>
/// Gets the inter-frame intra luma-mode distributions indexed by the normative block-size group.
/// </summary>
public Av1Distribution[] FrameYMode { get; }
/// <summary>
/// Gets the key-frame luma-mode distributions.
/// </summary>
public Av1Distribution[][] KeyFrameYMode { get; }
/// <summary>
/// Gets the distributions that select intra or inter prediction from the available spatial neighbors.
/// </summary>
public Av1Distribution[] IntraInter { get; }
/// <summary>
/// Gets the distributions that select a newly decoded motion vector before the remaining single-reference modes.
/// </summary>
public Av1Distribution[] NewMv { get; }
/// <summary>
/// Gets the distributions that select global motion before the spatial reference-motion-vector modes.
/// </summary>
public Av1Distribution[] ZeroMv { get; }
/// <summary>
/// Gets the distributions that select the nearest or near spatial reference motion vector.
/// </summary>
public Av1Distribution[] RefMv { get; }
/// <summary>
/// Gets the distributions that advance through the dynamic reference-motion-vector candidate list.
/// </summary>
public Av1Distribution[] Drl { get; }
/// <summary>
/// Gets the single-reference selection distributions indexed by spatial context and tree decision.
/// </summary>
public Av1Distribution[][] SingleReference { get; }
/// <summary>
/// Gets the distributions that select single-reference or compound-reference prediction for a block.
/// </summary>
public Av1Distribution[] CompInter { get; }
/// <summary>
/// Gets the distributions that select unidirectional or bidirectional compound reference pairs.
/// </summary>
public Av1Distribution[] CompoundReferenceType { get; }
/// <summary>
/// Gets the unidirectional compound-reference selection distributions.
/// </summary>
public Av1Distribution[][] UnidirectionalCompoundReference { get; }
/// <summary>
/// Gets the bidirectional compound forward-reference selection distributions.
/// </summary>
public Av1Distribution[][] CompoundReference { get; }
/// <summary>
/// Gets the bidirectional compound backward-reference selection distributions.
/// </summary>
public Av1Distribution[][] CompoundBackwardReference { get; }
/// <summary>
/// Gets the compound inter-mode distributions indexed by derived mode context.
/// </summary>
public Av1Distribution[] InterCompoundMode { get; }
/// <summary>
/// Gets the inter-intra prediction flag distributions indexed by block-size group.
/// </summary>
public Av1Distribution[] InterIntra { get; }
/// <summary>
/// Gets the inter-intra mode distributions indexed by block-size group.
/// </summary>
public Av1Distribution[] InterIntraMode { get; }
/// <summary>
/// Gets the inter-intra wedge-selection distributions indexed by block size.
/// </summary>
public Av1Distribution[] WedgeInterIntra { get; }
/// <summary>
/// Gets the masked-compound type distributions indexed by block size.
/// </summary>
public Av1Distribution[] CompoundType { get; }
/// <summary>
/// Gets the wedge-index distributions indexed by block size.
/// </summary>
public Av1Distribution[] WedgeIndex { get; }
/// <summary>
/// Gets the average-or-distance-weighted compound distributions indexed by derived context.
/// </summary>
public Av1Distribution[] CompoundIndex { get; }
/// <summary>
/// Gets the unmasked-or-masked compound-group distributions indexed by derived context.
/// </summary>
public Av1Distribution[] CompoundGroupIndex { get; }
/// <summary>
/// Gets the three-way motion-mode distributions indexed by block size.
/// </summary>
public Av1Distribution[] MotionMode { get; }
/// <summary>
/// Gets the binary Simple Translation or OBMC distributions indexed by block size.
/// </summary>
public Av1Distribution[] Obmc { get; }
/// <summary>
/// Gets the switchable interpolation-filter distributions.
/// </summary>
public Av1Distribution[] SwitchableInterpolation { get; }
/// <summary>
/// Gets the chroma intra-mode distributions.
/// </summary>
public Av1Distribution[][] UvMode { get; }
/// <summary>
/// Gets the transform-skip distributions.
/// </summary>
public Av1Distribution[] Skip { get; }
/// <summary>
/// Gets the skip-mode distributions.
/// </summary>
public Av1Distribution[] SkipMode { get; }
/// <summary>
/// Gets the absolute loop-filter delta distribution.
/// </summary>
public Av1Distribution DeltaLoopFilterAbsolute { get; }
/// <summary>
/// Gets the absolute quantizer delta distribution.
/// </summary>
public Av1Distribution DeltaQuantizerAbsolute { get; }
/// <summary>
/// Gets the spatial segment-identifier distributions.
/// </summary>
public Av1Distribution[] SegmentId { get; }
/// <summary>
/// Gets the temporal segment-map prediction distributions.
/// </summary>
public Av1Distribution[] SegmentIdPredicted { get; }
/// <summary>
/// Gets the directional angle-delta distributions.
/// </summary>
public Av1Distribution[] AngleDelta { get; }
/// <summary>
/// Gets the filter-intra mode distribution.
/// </summary>
public Av1Distribution FilterIntraMode { get; }
/// <summary>
/// Gets the filter-intra enable distributions.
/// </summary>
public Av1Distribution[] FilterIntra { get; }
/// <summary>
/// Gets the transform-size distributions.
/// </summary>
public Av1Distribution[][] TransformSize { get; }
/// <summary>
/// Gets the variable-transform partition distributions.
/// </summary>
public Av1Distribution[] TransformPartition { get; }
/// <summary>
/// Gets the end-of-block token distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][][] EndOfBlockFlag { get; }
/// <summary>
/// Gets the coefficient base-level distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][][] CoefficientsBase { get; }
/// <summary>
/// Gets the final-nonzero coefficient distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][][] BaseEndOfBlock { get; }
/// <summary>
/// Gets the DC sign distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][] DcSign { get; }
/// <summary>
/// Gets the coefficient base-range distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][][] CoefficientsBaseRange { get; }
/// <summary>
/// Gets the transform-block skip distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][] TransformBlockSkip { get; }
/// <summary>
/// Gets the end-of-block extra-bit distributions selected for the frame base quantizer.
/// </summary>
public Av1Distribution[][][] EndOfBlockExtra { get; }
/// <summary>
/// Gets the joint chroma-from-luma sign distribution.
/// </summary>
public Av1Distribution ChromaFromLumaSign { get; }
/// <summary>
/// Gets the chroma-from-luma alpha-magnitude distributions.
/// </summary>
public Av1Distribution[] ChromaFromLumaAlpha { get; }
/// <summary>
/// Gets the intra transform-type distributions.
/// </summary>
public Av1Distribution[][][] IntraExtendedTransform { get; }
/// <summary>
/// Gets the inter transform-type distributions.
/// </summary>
public Av1Distribution[][] InterExtendedTransform { get; }
/// <summary>
/// Restores the normative frame defaults selected by a base quantizer index.
/// </summary>
/// <param name="qIndex">The frame base quantizer index selecting coefficient distribution defaults.</param>
public void ResetToDefaults(int qIndex)
{
int qContext = GetQContext(qIndex);
// The prototypes are never exposed to a range reader. Copying their state lets a decoder session reuse the
// same three mutable object graphs even when successive frames select different coefficient-model bands.
this.CopyFrom(DefaultPrototypes[qContext]);
}
/// <summary>
/// Maps a frame base quantizer to its normative coefficient-probability initialization band.
/// </summary>
/// <param name="qIndex">The frame base quantizer index.</param>
/// <returns>The zero-based quantizer-band index.</returns>
private static int GetQContext(int qIndex)
=> qIndex switch
{
<= FirstQuantizerBandMaximum => 0,
<= SecondQuantizerBandMaximum => 1,
<= ThirdQuantizerBandMaximum => 2,
_ => 3
};
/// <summary>
/// Replaces every probability threshold and adaptation count with state copied from another frame context.
/// </summary>
/// <param name="source">The frame context state to copy.</param>
public void CopyFrom(Av1FrameEntropyContext source)
{
this.IntraBlockCopy.CopyFrom(source.IntraBlockCopy);
this.MotionVector.CopyFrom(source.MotionVector);
this.DisplacementVector.CopyFrom(source.DisplacementVector);
this.SwitchableRestoration.CopyFrom(source.SwitchableRestoration);
this.WienerRestoration.CopyFrom(source.WienerRestoration);
this.SgrProjectionRestoration.CopyFrom(source.SgrProjectionRestoration);
CopyState(source.PaletteYMode, this.PaletteYMode);
CopyState(source.PaletteUvMode, this.PaletteUvMode);
CopyState(source.PaletteYSize, this.PaletteYSize);
CopyState(source.PaletteUvSize, this.PaletteUvSize);
CopyState(source.PaletteYColorIndex, this.PaletteYColorIndex);
CopyState(source.PaletteUvColorIndex, this.PaletteUvColorIndex);
CopyState(source.PartitionTypes, this.PartitionTypes);
CopyState(source.FrameYMode, this.FrameYMode);
CopyState(source.KeyFrameYMode, this.KeyFrameYMode);
CopyState(source.IntraInter, this.IntraInter);
CopyState(source.NewMv, this.NewMv);
CopyState(source.ZeroMv, this.ZeroMv);
CopyState(source.RefMv, this.RefMv);
CopyState(source.Drl, this.Drl);
CopyState(source.SingleReference, this.SingleReference);
CopyState(source.CompInter, this.CompInter);
CopyState(source.CompoundReferenceType, this.CompoundReferenceType);
CopyState(source.UnidirectionalCompoundReference, this.UnidirectionalCompoundReference);
CopyState(source.CompoundReference, this.CompoundReference);
CopyState(source.CompoundBackwardReference, this.CompoundBackwardReference);
CopyState(source.InterCompoundMode, this.InterCompoundMode);
CopyState(source.InterIntra, this.InterIntra);
CopyState(source.InterIntraMode, this.InterIntraMode);
CopyState(source.WedgeInterIntra, this.WedgeInterIntra);
CopyState(source.CompoundType, this.CompoundType);
CopyState(source.WedgeIndex, this.WedgeIndex);
CopyState(source.CompoundIndex, this.CompoundIndex);
CopyState(source.CompoundGroupIndex, this.CompoundGroupIndex);
CopyState(source.MotionMode, this.MotionMode);
CopyState(source.Obmc, this.Obmc);
CopyState(source.SwitchableInterpolation, this.SwitchableInterpolation);
CopyState(source.UvMode, this.UvMode);
CopyState(source.Skip, this.Skip);
CopyState(source.SkipMode, this.SkipMode);
this.DeltaLoopFilterAbsolute.CopyFrom(source.DeltaLoopFilterAbsolute);
this.DeltaQuantizerAbsolute.CopyFrom(source.DeltaQuantizerAbsolute);
CopyState(source.SegmentId, this.SegmentId);
CopyState(source.SegmentIdPredicted, this.SegmentIdPredicted);
CopyState(source.AngleDelta, this.AngleDelta);
this.FilterIntraMode.CopyFrom(source.FilterIntraMode);
CopyState(source.FilterIntra, this.FilterIntra);
CopyState(source.TransformSize, this.TransformSize);
CopyState(source.TransformPartition, this.TransformPartition);
CopyState(source.EndOfBlockFlag, this.EndOfBlockFlag);
CopyState(source.CoefficientsBase, this.CoefficientsBase);
CopyState(source.BaseEndOfBlock, this.BaseEndOfBlock);
CopyState(source.DcSign, this.DcSign);
CopyState(source.CoefficientsBaseRange, this.CoefficientsBaseRange);
CopyState(source.TransformBlockSkip, this.TransformBlockSkip);
CopyState(source.EndOfBlockExtra, this.EndOfBlockExtra);
this.ChromaFromLumaSign.CopyFrom(source.ChromaFromLumaSign);
CopyState(source.ChromaFromLumaAlpha, this.ChromaFromLumaAlpha);
CopyState(source.IntraExtendedTransform, this.IntraExtendedTransform);
CopyState(source.InterExtendedTransform, this.InterExtendedTransform);
}
/// <summary>
/// Copies this tile-adapted context into a destination used as completed frame state.
/// </summary>
/// <param name="destination">The independently owned frame context that receives the snapshot.</param>
/// <remarks>
/// AV1 resets CDF observation counters after publishing the context-update tile. The copied thresholds remain
/// adapted, while the next frame starts its update-rate history from zero.
/// </remarks>
public void SnapshotTo(Av1FrameEntropyContext destination)
{
destination.CopyFrom(this);
destination.ResetUpdateCounts();
}
/// <summary>
/// Resets the observation count of every distribution without changing its probability thresholds.
/// </summary>
private void ResetUpdateCounts()
{
this.IntraBlockCopy.ResetUpdateCount();
this.MotionVector.ResetUpdateCounts();
this.DisplacementVector.ResetUpdateCounts();
this.SwitchableRestoration.ResetUpdateCount();
this.WienerRestoration.ResetUpdateCount();
this.SgrProjectionRestoration.ResetUpdateCount();
ResetUpdateCounts(this.PaletteYMode);
ResetUpdateCounts(this.PaletteUvMode);
ResetUpdateCounts(this.PaletteYSize);
ResetUpdateCounts(this.PaletteUvSize);
ResetUpdateCounts(this.PaletteYColorIndex);
ResetUpdateCounts(this.PaletteUvColorIndex);
ResetUpdateCounts(this.PartitionTypes);
ResetUpdateCounts(this.FrameYMode);
ResetUpdateCounts(this.KeyFrameYMode);
ResetUpdateCounts(this.IntraInter);
ResetUpdateCounts(this.NewMv);
ResetUpdateCounts(this.ZeroMv);
ResetUpdateCounts(this.RefMv);
ResetUpdateCounts(this.Drl);
ResetUpdateCounts(this.SingleReference);
ResetUpdateCounts(this.CompInter);
ResetUpdateCounts(this.CompoundReferenceType);
ResetUpdateCounts(this.UnidirectionalCompoundReference);
ResetUpdateCounts(this.CompoundReference);
ResetUpdateCounts(this.CompoundBackwardReference);
ResetUpdateCounts(this.InterCompoundMode);
ResetUpdateCounts(this.InterIntra);
ResetUpdateCounts(this.InterIntraMode);
ResetUpdateCounts(this.WedgeInterIntra);
ResetUpdateCounts(this.CompoundType);
ResetUpdateCounts(this.WedgeIndex);
ResetUpdateCounts(this.CompoundIndex);
ResetUpdateCounts(this.CompoundGroupIndex);
ResetUpdateCounts(this.MotionMode);
ResetUpdateCounts(this.Obmc);
ResetUpdateCounts(this.SwitchableInterpolation);
ResetUpdateCounts(this.UvMode);
ResetUpdateCounts(this.Skip);
ResetUpdateCounts(this.SkipMode);
this.DeltaLoopFilterAbsolute.ResetUpdateCount();
this.DeltaQuantizerAbsolute.ResetUpdateCount();
ResetUpdateCounts(this.SegmentId);
ResetUpdateCounts(this.SegmentIdPredicted);
ResetUpdateCounts(this.AngleDelta);
this.FilterIntraMode.ResetUpdateCount();
ResetUpdateCounts(this.FilterIntra);
ResetUpdateCounts(this.TransformSize);
ResetUpdateCounts(this.TransformPartition);
ResetUpdateCounts(this.EndOfBlockFlag);
ResetUpdateCounts(this.CoefficientsBase);
ResetUpdateCounts(this.BaseEndOfBlock);
ResetUpdateCounts(this.DcSign);
ResetUpdateCounts(this.CoefficientsBaseRange);
ResetUpdateCounts(this.TransformBlockSkip);
ResetUpdateCounts(this.EndOfBlockExtra);
this.ChromaFromLumaSign.ResetUpdateCount();
ResetUpdateCounts(this.ChromaFromLumaAlpha);
ResetUpdateCounts(this.IntraExtendedTransform);
ResetUpdateCounts(this.InterExtendedTransform);
}
/// <summary>
/// Copies one distribution row into an existing row with the same default-table shape.
/// </summary>
/// <param name="source">The source distribution row.</param>
/// <param name="destination">The destination distribution row.</param>
private static void CopyState(Av1Distribution[] source, Av1Distribution[] destination)
{
for (int index = 0; index < source.Length; index++)
{
destination[index].CopyFrom(source[index]);
}
}
/// <summary>
/// Copies a two-dimensional distribution table into an existing table with the same default-table shape.
/// </summary>
/// <param name="source">The source distribution table.</param>
/// <param name="destination">The destination distribution table.</param>
private static void CopyState(Av1Distribution[][] source, Av1Distribution[][] destination)
{
for (int index = 0; index < source.Length; index++)
{
CopyState(source[index], destination[index]);
}
}
/// <summary>
/// Copies a three-dimensional distribution table into an existing table with the same default-table shape.
/// </summary>
/// <param name="source">The source distribution table.</param>
/// <param name="destination">The destination distribution table.</param>
private static void CopyState(Av1Distribution[][][] source, Av1Distribution[][][] destination)
{
for (int index = 0; index < source.Length; index++)
{
CopyState(source[index], destination[index]);
}
}
/// <summary>
/// Resets observation counts in one distribution row.
/// </summary>
/// <param name="distributions">The distribution row to reset.</param>
private static void ResetUpdateCounts(Av1Distribution[] distributions)
{
for (int index = 0; index < distributions.Length; index++)
{
distributions[index].ResetUpdateCount();
}
}
/// <summary>
/// Resets observation counts in a two-dimensional distribution table.
/// </summary>
/// <param name="distributions">The distribution table to reset.</param>
private static void ResetUpdateCounts(Av1Distribution[][] distributions)
{
for (int index = 0; index < distributions.Length; index++)
{
ResetUpdateCounts(distributions[index]);
}
}
/// <summary>
/// Resets observation counts in a three-dimensional distribution table.
/// </summary>
/// <param name="distributions">The distribution table to reset.</param>
private static void ResetUpdateCounts(Av1Distribution[][][] distributions)
{
for (int index = 0; index < distributions.Length; index++)
{
ResetUpdateCounts(distributions[index]);
}
}
}

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

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

319
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1MotionVectorContext.cs

@ -0,0 +1,319 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Owns one independently adaptive AV1 motion-vector entropy context.
/// </summary>
/// <remarks>
/// Normal inter-prediction vectors and intra-block-copy displacement vectors use identical initial distributions, but
/// each syntax domain owns a separate instance so observations from one domain cannot adapt the other.
/// </remarks>
internal sealed class Av1MotionVectorContext
{
/// <summary>
/// The number of magnitude classes defined by AV1.
/// </summary>
private const int MagnitudeClassCount = 11;
/// <summary>
/// The number of integer magnitude bits coded directly for class zero.
/// </summary>
private const int ClassZeroBitCount = 1;
/// <summary>
/// The number of integer magnitude offsets represented by class zero.
/// </summary>
private const int ClassZeroSize = 1 << ClassZeroBitCount;
/// <summary>
/// Gets the distribution selecting which vector components are nonzero.
/// </summary>
public Av1Distribution Joint { get; } = new(4096, 11264, 19328);
/// <summary>
/// Gets the adaptive distributions for the vertical vector component.
/// </summary>
public Component Vertical { get; } = new();
/// <summary>
/// Gets the adaptive distributions for the horizontal vector component.
/// </summary>
public Component Horizontal { get; } = new();
/// <summary>
/// Replaces every motion-vector distribution with state copied from another context.
/// </summary>
/// <param name="source">The motion-vector context state to copy.</param>
public void CopyFrom(Av1MotionVectorContext source)
{
this.Joint.CopyFrom(source.Joint);
this.Vertical.CopyFrom(source.Vertical);
this.Horizontal.CopyFrom(source.Horizontal);
}
/// <summary>
/// Resets every observation count used to adapt the motion-vector distributions.
/// </summary>
public void ResetUpdateCounts()
{
this.Joint.ResetUpdateCount();
this.Vertical.ResetUpdateCounts();
this.Horizontal.ResetUpdateCounts();
}
/// <summary>
/// Reads a motion-vector delta relative to a spatially derived reference.
/// </summary>
/// <param name="reader">The tile range decoder.</param>
/// <param name="reference">The reference motion vector.</param>
/// <param name="precision">The fractional precision allowed by the current frame.</param>
/// <returns>The decoded motion vector in one-eighth-sample units.</returns>
public Av1MotionVector Read(ref Av1SymbolReader reader, Av1MotionVector reference, Av1MotionVectorPrecision precision)
{
int jointType = reader.ReadSymbol(this.Joint);
// Joint values 1 and 3 carry a horizontal delta; values 2 and 3 carry a vertical delta. Reading only the
// signaled components preserves the normative entropy-symbol order and leaves zero components unadapted.
int row = jointType >= 2 ? this.Vertical.Read(ref reader, precision) : 0;
int column = (jointType & 1) != 0 ? this.Horizontal.Read(ref reader, precision) : 0;
return reference + new Av1MotionVector(row, column);
}
/// <summary>
/// Writes an integer displacement vector relative to a spatially derived reference.
/// </summary>
/// <param name="writer">The tile range encoder.</param>
/// <param name="value">The displacement vector to encode.</param>
/// <param name="reference">The spatially derived reference vector.</param>
public void Write(Av1SymbolWriter writer, Av1MotionVector value, Av1MotionVector reference)
{
int row = value.Row - reference.Row;
int column = value.Column - reference.Column;
// Bit zero signals a horizontal delta and bit one signals a vertical delta, producing the four normative
// zero/horizontal/vertical/both joint symbols without a lookup.
int jointType = (row != 0 ? 2 : 0) | (column != 0 ? 1 : 0);
writer.WriteSymbol(jointType, this.Joint);
if (row != 0)
{
this.Vertical.Write(writer, row);
}
if (column != 0)
{
this.Horizontal.Write(writer, column);
}
}
/// <summary>
/// Owns the adaptive magnitude distributions for one motion-vector component.
/// </summary>
public sealed class Component
{
/// <summary>
/// Gets the distribution selecting the magnitude class of a nonzero component.
/// </summary>
public Av1Distribution MagnitudeClass { get; } = new(28672, 30976, 31858, 32320, 32551, 32656, 32740, 32757, 32762, 32767);
/// <summary>
/// Gets the fractional distributions selected by the two class-zero integer offsets.
/// </summary>
public Av1Distribution[] ClassZeroFractional { get; } =
[
new(16384, 24576, 26624),
new(12288, 21248, 24128)
];
/// <summary>
/// Gets the fractional distribution used by nonzero magnitude classes.
/// </summary>
public Av1Distribution Fractional { get; } = new(8192, 17408, 21248);
/// <summary>
/// Gets the distribution selecting the sign of a nonzero component.
/// </summary>
public Av1Distribution Sign { get; } = new(16384);
/// <summary>
/// Gets the eighth-sample distribution used by class-zero magnitudes.
/// </summary>
public Av1Distribution ClassZeroHighPrecision { get; } = new(20480);
/// <summary>
/// Gets the eighth-sample distribution used by nonzero magnitude classes.
/// </summary>
public Av1Distribution HighPrecision { get; } = new(16384);
/// <summary>
/// Gets the distribution selecting either of the two class-zero integer magnitude offsets.
/// </summary>
public Av1Distribution ClassZero { get; } = new(27648);
/// <summary>
/// Gets the binary distributions that reconstruct larger integer magnitude offsets from least to most significant bit.
/// </summary>
public Av1Distribution[] OffsetBits { get; } =
[
new(17408), new(17920), new(18944), new(20480), new(22528),
new(24576), new(28672), new(29952), new(29952), new(30720)
];
/// <summary>
/// Replaces every component distribution with state copied from another component.
/// </summary>
/// <param name="source">The component state to copy.</param>
public void CopyFrom(Component source)
{
this.MagnitudeClass.CopyFrom(source.MagnitudeClass);
for (int offset = 0; offset < this.ClassZeroFractional.Length; offset++)
{
this.ClassZeroFractional[offset].CopyFrom(source.ClassZeroFractional[offset]);
}
this.Fractional.CopyFrom(source.Fractional);
this.Sign.CopyFrom(source.Sign);
this.ClassZeroHighPrecision.CopyFrom(source.ClassZeroHighPrecision);
this.HighPrecision.CopyFrom(source.HighPrecision);
this.ClassZero.CopyFrom(source.ClassZero);
for (int bit = 0; bit < this.OffsetBits.Length; bit++)
{
this.OffsetBits[bit].CopyFrom(source.OffsetBits[bit]);
}
}
/// <summary>
/// Resets every observation count used to adapt one component's distributions.
/// </summary>
public void ResetUpdateCounts()
{
this.MagnitudeClass.ResetUpdateCount();
for (int offset = 0; offset < this.ClassZeroFractional.Length; offset++)
{
this.ClassZeroFractional[offset].ResetUpdateCount();
}
this.Fractional.ResetUpdateCount();
this.Sign.ResetUpdateCount();
this.ClassZeroHighPrecision.ResetUpdateCount();
this.HighPrecision.ResetUpdateCount();
this.ClassZero.ResetUpdateCount();
for (int bit = 0; bit < this.OffsetBits.Length; bit++)
{
this.OffsetBits[bit].ResetUpdateCount();
}
}
/// <summary>
/// Reads one signed motion-vector component at the requested precision.
/// </summary>
/// <param name="reader">The tile range decoder.</param>
/// <param name="precision">The fractional precision allowed by the current frame.</param>
/// <returns>The signed component in one-eighth-sample units.</returns>
public int Read(ref Av1SymbolReader reader, Av1MotionVectorPrecision precision)
{
bool isNegative = reader.ReadSymbol(this.Sign) != 0;
int magnitudeClass = reader.ReadSymbol(this.MagnitudeClass);
bool isClassZero = magnitudeClass == 0;
int integerOffset;
int magnitudeBase;
if (isClassZero)
{
integerOffset = reader.ReadSymbol(this.ClassZero);
magnitudeBase = 0;
}
else
{
int bitCount = magnitudeClass + ClassZeroBitCount - 1;
integerOffset = 0;
for (int bit = 0; bit < bitCount; bit++)
{
// AV1 transmits the integer offset least-significant bit first, with an independently adapting
// distribution for every bit position.
integerOffset |= reader.ReadSymbol(this.OffsetBits[bit]) << bit;
}
// Class one uses a base of two whole samples, or sixteen eighth-sample units, and every later class doubles
// that base. CLASS0_SIZE shifted by class + 2 expresses the same scale directly in eighth-sample units.
magnitudeBase = ClassZeroSize << (magnitudeClass + 2);
}
int fractional;
int highPrecision;
if (precision != Av1MotionVectorPrecision.Integer)
{
// Class-zero magnitudes select one of two fractional CDFs using the already decoded integer offset;
// larger classes share one fractional CDF because their expanded integer range supplies the context.
Av1Distribution fractionalDistribution = isClassZero ? this.ClassZeroFractional[integerOffset] : this.Fractional;
fractional = reader.ReadSymbol(fractionalDistribution);
// Quarter-sample motion omits the eighth-sample symbol. The normative implicit one, combined with the
// final increment below, constrains the result to even one-eighth-sample units.
highPrecision = precision == Av1MotionVectorPrecision.EighthSample
? reader.ReadSymbol(isClassZero ? this.ClassZeroHighPrecision : this.HighPrecision)
: 1;
}
else
{
// Integer motion omits both fractional symbols. The implicit maximum values make the low three bits
// all one before the final increment, constraining the result to whole-sample multiples of eight.
fractional = 3;
highPrecision = 1;
}
// The entropy syntax represents magnitude minus one. Integer offset occupies bits three and above,
// fractional occupies bits one and two, and high precision occupies bit zero, all in one-eighth-sample units.
int magnitude = magnitudeBase + ((integerOffset << 3) | (fractional << 1) | highPrecision) + 1;
return isNegative ? -magnitude : magnitude;
}
/// <summary>
/// Writes one signed integer-precision component.
/// </summary>
/// <param name="writer">The tile range encoder.</param>
/// <param name="value">The nonzero component in one-eighth-sample units.</param>
public void Write(Av1SymbolWriter writer, int value)
{
int magnitude = Math.Abs(value);
DebugGuard.IsTrue(magnitude > 0 && (magnitude & 7) == 0, "Displacement-vector components must use whole-sample precision.");
// Class zero contains the two whole-sample magnitudes 8 and 16. Above it, the highest set bit of magnitude
// minus one selects the doubling range; subtracting three converts the eighth-sample bit index to the class.
int magnitudeClass = magnitude <= (ClassZeroSize << 3) ? 0 : Av1Math.MostSignificantBit((uint)(magnitude - 1)) - 3;
DebugGuard.MustBeLessThan(magnitudeClass, MagnitudeClassCount, nameof(magnitudeClass));
writer.WriteSymbol(value < 0, this.Sign);
writer.WriteSymbol(magnitudeClass, this.MagnitudeClass);
if (magnitudeClass == 0)
{
writer.WriteSymbol((magnitude >> 3) - 1, this.ClassZero);
return;
}
// Remove the class base and the implicit low-bit value 7 plus the final one before coding the remaining
// whole-sample offset least-significant bit first.
int magnitudeBase = ClassZeroSize << (magnitudeClass + 2);
int integerOffset = (magnitude - magnitudeBase - 8) >> 3;
for (int bit = 0; bit < magnitudeClass; bit++)
{
// The decoder reconstructs offsets least-significant bit first, so each adaptive bit model must be
// updated in the same order during encoding.
writer.WriteSymbol(((integerOffset >> bit) & 1) != 0, this.OffsetBits[bit]);
}
}
}
}

501
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1NzMap.cs

@ -0,0 +1,501 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Derives the AV1 nonzero-coefficient probability context from neighboring coefficient levels and transform geometry.
/// </summary>
internal static class Av1NzMap
{
/// <summary>
/// The first one-dimensional nonzero-map context, immediately after the 26 two-dimensional contexts.
/// </summary>
private const int NzMapContext0 = 26;
/// <summary>
/// The second one-dimensional position band.
/// </summary>
private const int NzMapContext5 = NzMapContext0 + 5;
/// <summary>
/// The final one-dimensional position band.
/// </summary>
private const int NzMapContext10 = NzMapContext0 + 10;
/// <summary>
/// Maps a horizontal or vertical coefficient coordinate to its one-dimensional context offset.
/// </summary>
private static readonly int[] NzMapContextOffset1d = [
NzMapContext0, NzMapContext5, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10,
NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10,
NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10,
NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10,
NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10,
];
/// <summary>
/// The row-major positional context offsets for a 4x4 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset4x4 = [
0, 1, 6, 6,
1, 6, 6, 21,
6, 6, 21, 21,
6, 21, 21, 21];
/// <summary>
/// The row-major positional context offsets for an 8x8 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset8x8 = [
0, 1, 6, 6, 21, 21, 21, 21,
1, 6, 6, 21, 21, 21, 21, 21,
6, 6, 21, 21, 21, 21, 21, 21,
6, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for a 16x16 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset16x16 = [
0, 1, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
1, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for a 32x32 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset32x32 = [
0, 1, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
1, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for an 8x4 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset8x4 = [
0, 16, 6, 6, 21, 21, 21, 21,
16, 16, 6, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for a 16x8 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset16x8 = [
0, 16, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for a 16x32 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset16x32 = [
0, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for a 32x16 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset32x16 = [
0, 16, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for the coded low-frequency region of a 32x64 transform.
/// </summary>
private static readonly int[] NzMapContextOffset32x64 = [
0, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for the coded low-frequency region of a 64x32 transform.
/// </summary>
private static readonly int[] NzMapContextOffset64x32 = [
0, 16, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for a 4x16 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset4x16 = [
0, 11, 11, 11,
11, 11, 11, 11,
6, 6, 21, 21,
6, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for a 16x4 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset16x4 = [
0, 16, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for an 8x32 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset8x32 = [
0, 11, 11, 11, 11, 11, 11, 11,
11, 11, 11, 11, 11, 11, 11, 11,
6, 6, 21, 21, 21, 21, 21, 21,
6, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// The row-major positional context offsets for a 32x8 two-dimensional transform.
/// </summary>
private static readonly int[] NzMapContextOffset32x8 = [
0, 16, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
];
/// <summary>
/// Maps each AV1 transform-size value to its row-major two-dimensional positional context table.
/// </summary>
private static readonly int[][] NzMapContextOffset = [
// Several rectangular transforms reuse a prefix of the next larger table. Every reused prefix has the same
// row-major stride as the adjusted coefficient plane, which avoids duplicating identical context offsets.
NzMapContextOffset4x4, // TX_4x4
NzMapContextOffset8x8, // TX_8x8
NzMapContextOffset16x16, // TX_16x16
NzMapContextOffset32x32, // TX_32x32
NzMapContextOffset32x32, // TX_64x64
NzMapContextOffset4x16, // TX_4x8
NzMapContextOffset8x4, // TX_8x4
NzMapContextOffset8x32, // TX_8x16
NzMapContextOffset16x8, // TX_16x8
NzMapContextOffset16x32, // TX_16x32
NzMapContextOffset32x16, // TX_32x16
NzMapContextOffset32x64, // TX_32x64
NzMapContextOffset64x32, // TX_64x32
NzMapContextOffset4x16, // TX_4x16
NzMapContextOffset16x4, // TX_16x4
NzMapContextOffset8x32, // TX_8x32
NzMapContextOffset32x8, // TX_32x8
NzMapContextOffset16x32, // TX_16x64
NzMapContextOffset64x32, // TX_64x16
];
/// <summary>
/// Sums the clipped magnitudes of the transform-class-specific forward coefficient neighbors.
/// </summary>
/// <param name="levels">The padded absolute-coefficient level plane.</param>
/// <param name="position">The coefficient position.</param>
/// <param name="transformClass">The transform direction class selecting the neighbor pattern.</param>
/// <returns>The summed neighbor magnitude used to select a nonzero-map context.</returns>
public static int GetNzMagnitude(Av1LevelBuffer levels, Point position, Av1TransformClass transformClass)
{
int mag;
Span<byte> row0 = levels.GetRow(position.Y)[position.X..];
Span<byte> row1 = levels.GetRow(position.Y + 1)[position.X..];
Span<byte> row2 = levels.GetRow(position.Y + 2)[position.X..];
// Large levels must not dominate probability selection; AV1 contributes at most three from each neighbor.
mag = ClipMax3(row0[1]); // { 0, 1 }
mag += ClipMax3(row1[0]); // { 1, 0 }
switch (transformClass)
{
case Av1TransformClass.Class2D:
mag += ClipMax3(row1[1]); // { 1, 1 }
mag += ClipMax3(row0[2]); // { 0, 2 }
mag += ClipMax3(row2[0]); // { 2, 0 }
break;
case Av1TransformClass.ClassVertical:
Span<byte> row3 = levels.GetRow(position.Y + 3)[position.X..];
Span<byte> row4 = levels.GetRow(position.Y + 4)[position.X..];
mag += ClipMax3(row2[0]); // { 2, 0 }
mag += ClipMax3(row3[0]); // { 3, 0 }
mag += ClipMax3(row4[0]); // { 4, 0 }
break;
case Av1TransformClass.ClassHorizontal:
mag += ClipMax3(row0[2]); // { 0, 2 }
mag += ClipMax3(row0[3]); // { 0, 3 }
mag += ClipMax3(row0[4]); // { 0, 4 }
break;
}
return mag;
}
/// <summary>
/// Combines a neighboring-level statistic with the coefficient's transform-class-specific position band.
/// </summary>
/// <param name="stats">The clipped sum of the applicable forward-neighbor magnitudes.</param>
/// <param name="position">The coefficient position.</param>
/// <param name="transformSize">The coded transform size selecting the positional table.</param>
/// <param name="transformClass">The transform direction class.</param>
/// <returns>The nonzero-map probability context.</returns>
public static int GetNzMapContextFromStats(int stats, Point position, Av1TransformSize transformSize, Av1TransformClass transformClass)
{
// The DC coefficient has a dedicated 2D context independent of neighboring levels.
if (transformClass == Av1TransformClass.Class2D && position.X == 0 && position.Y == 0)
{
return 0;
}
// Rounding the neighbor sum before clipping produces the five AV1 magnitude bands 0 through 4.
int ctx = (stats + 1) >> 1;
ctx = Math.Min(ctx, 4);
switch (transformClass)
{
case Av1TransformClass.Class2D:
// The tables preserve AV1's distinct early-row and early-column bands for rectangular transforms.
return ctx + GetNzMapContext(transformSize, position);
case Av1TransformClass.ClassHorizontal:
return ctx + NzMapContextOffset1d[position.X];
case Av1TransformClass.ClassVertical:
return ctx + NzMapContextOffset1d[position.Y];
default:
break;
}
return 0;
}
/// <summary>
/// Gets the two-dimensional positional context offset for a coefficient position.
/// </summary>
/// <param name="transformSize">The coded transform size.</param>
/// <param name="position">The coefficient position.</param>
/// <returns>The positional context offset.</returns>
public static int GetNzMapContext(Av1TransformSize transformSize, Point position)
{
// AV1 codes only the low-frequency 32-sample region of a 64-point transform dimension. The table still
// uses the signaled shape to select tall or wide bands, but its row-major stride follows the coded region.
int codedWidth = transformSize.GetAdjusted().GetWidth();
return GetNzMapContext(transformSize, position.X + (position.Y * codedWidth));
}
/// <summary>
/// Gets the two-dimensional positional context offset for a row-major coefficient index.
/// </summary>
/// <param name="transformSize">The coded transform size.</param>
/// <param name="position">The row-major coefficient index.</param>
/// <returns>The positional context offset.</returns>
public static int GetNzMapContext(Av1TransformSize transformSize, int position) => NzMapContextOffset[(int)transformSize][position];
/// <summary>
/// Clips a coefficient magnitude to the maximum contribution allowed per neighbor.
/// </summary>
/// <param name="value">The coefficient magnitude.</param>
/// <returns>The magnitude limited to three.</returns>
private static int ClipMax3(int value) => Math.Min(value, 3);
}

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

@ -0,0 +1,674 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Encodes AV1 tile syntax elements and transform coefficients with tile-local adaptive distributions.
/// </summary>
internal class Av1SymbolEncoder : IDisposable
{
/// <summary>
/// The tile-adaptive intra-block-copy distribution.
/// </summary>
private readonly Av1Distribution tileIntraBlockCopy;
/// <summary>
/// The tile-adaptive integer displacement-vector context.
/// </summary>
private readonly Av1MotionVectorContext displacementVector = new();
/// <summary>
/// The tile-adaptive partition-type distributions.
/// </summary>
private readonly Av1Distribution[] tilePartitionTypes;
/// <summary>
/// The tile-adaptive key-frame luma-mode distributions.
/// </summary>
private readonly Av1Distribution[][] keyFrameYMode;
/// <summary>
/// The tile-adaptive chroma intra-mode distributions.
/// </summary>
private readonly Av1Distribution[][] uvMode;
/// <summary>
/// The tile-adaptive transform-block skip distributions selected for the frame base quantizer.
/// </summary>
private readonly Av1Distribution[][] transformBlockSkip;
/// <summary>
/// The tile-adaptive end-of-block token distributions selected for the frame base quantizer.
/// </summary>
private readonly Av1Distribution[][][] endOfBlockFlag;
/// <summary>
/// The tile-adaptive coefficient base-range distributions selected for the frame base quantizer.
/// </summary>
private readonly Av1Distribution[][][] coefficientsBaseRange;
/// <summary>
/// The tile-adaptive coefficient base-level distributions selected for the frame base quantizer.
/// </summary>
private readonly Av1Distribution[][][] coefficientsBase;
/// <summary>
/// The tile-adaptive final-nonzero coefficient distributions selected for the frame base quantizer.
/// </summary>
private readonly Av1Distribution[][][] coefficientsBaseEndOfBlock;
/// <summary>
/// The tile-adaptive filter-intra enable distributions.
/// </summary>
private readonly Av1Distribution[] filterIntra;
/// <summary>
/// The tile-adaptive filter-intra mode distribution.
/// </summary>
private readonly Av1Distribution filterIntraMode;
/// <summary>
/// The tile-adaptive absolute quantizer delta distribution.
/// </summary>
private readonly Av1Distribution deltaQuantizerAbsolute;
/// <summary>
/// The tile-adaptive DC sign distributions selected for the frame base quantizer.
/// </summary>
private readonly Av1Distribution[][] dcSign;
/// <summary>
/// The tile-adaptive end-of-block extra-bit distributions selected for the frame base quantizer.
/// </summary>
private readonly Av1Distribution[][][] endOfBlockExtra;
/// <summary>
/// The tile-adaptive intra transform-type distributions.
/// </summary>
private readonly Av1Distribution[][][] intraExtendedTransform;
/// <summary>
/// The tile-adaptive spatial segment-identifier distributions.
/// </summary>
private readonly Av1Distribution[] segmentId;
/// <summary>
/// The tile-adaptive directional angle-delta distributions.
/// </summary>
private readonly Av1Distribution[] angleDelta;
/// <summary>
/// The tile-adaptive transform-skip distributions.
/// </summary>
private readonly Av1Distribution[] skip;
/// <summary>
/// The tile-adaptive skip-mode distributions.
/// </summary>
private readonly Av1Distribution[] skipMode;
/// <summary>
/// The tile-adaptive joint chroma-from-luma sign distribution.
/// </summary>
private readonly Av1Distribution chromaFromLumaSign;
/// <summary>
/// The tile-adaptive chroma-from-luma alpha-magnitude distributions.
/// </summary>
private readonly Av1Distribution[] chromaFromLumaAlpha;
/// <summary>
/// Indicates whether the range writer has been disposed.
/// </summary>
private bool isDisposed;
/// <summary>
/// The configuration providing output and coefficient-context memory.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// The range writer producing the current tile payload.
/// </summary>
private Av1SymbolWriter writer;
/// <summary>
/// The frame base quantizer used to select coefficient probability models.
/// </summary>
private readonly int baseQIndex;
/// <summary>
/// Initializes a new instance of the <see cref="Av1SymbolEncoder"/> class for one AV1 tile.
/// </summary>
/// <param name="configuration">The configuration providing output and temporary memory.</param>
/// <param name="initialSize">The initial output buffer size in bytes.</param>
/// <param name="qIndex">The frame base quantizer index.</param>
/// <param name="updateCdf">A value indicating whether encoded symbols adapt their tile distributions.</param>
public Av1SymbolEncoder(Configuration configuration, int initialSize, int qIndex, bool updateCdf = true)
{
// Every default accessor creates independently mutable state. Encoding and decoding therefore begin from
// equivalent tile-local models without constructing and immediately deep-copying a second object graph.
this.tileIntraBlockCopy = Av1DefaultDistributions.IntraBlockCopy;
this.tilePartitionTypes = Av1DefaultDistributions.PartitionTypes;
this.keyFrameYMode = Av1DefaultDistributions.KeyFrameYMode;
this.uvMode = Av1DefaultDistributions.UvMode;
this.filterIntra = Av1DefaultDistributions.FilterIntra;
this.filterIntraMode = Av1DefaultDistributions.FilterIntraMode;
this.deltaQuantizerAbsolute = Av1DefaultDistributions.DeltaQuantizerAbsolute;
this.intraExtendedTransform = Av1DefaultDistributions.IntraExtendedTransform;
this.segmentId = Av1DefaultDistributions.SegmentId;
this.angleDelta = Av1DefaultDistributions.AngleDelta;
this.skip = Av1DefaultDistributions.Skip;
this.skipMode = Av1DefaultDistributions.SkipMode;
this.chromaFromLumaSign = Av1DefaultDistributions.ChromaFromLumaSign;
this.chromaFromLumaAlpha = Av1DefaultDistributions.ChromaFromLumaAlpha;
this.transformBlockSkip = Av1DefaultDistributions.GetTransformBlockSkip(qIndex);
this.endOfBlockFlag = Av1DefaultDistributions.GetEndOfBlockFlag(qIndex);
this.coefficientsBaseRange = Av1DefaultDistributions.GetCoefficientsBaseRange(qIndex);
this.coefficientsBase = Av1DefaultDistributions.GetCoefficientsBase(qIndex);
this.coefficientsBaseEndOfBlock = Av1DefaultDistributions.GetBaseEndOfBlock(qIndex);
this.dcSign = Av1DefaultDistributions.GetDcSign(qIndex);
this.endOfBlockExtra = Av1DefaultDistributions.GetEndOfBlockExtra(qIndex);
this.configuration = configuration;
this.writer = new(configuration, initialSize, updateCdf);
this.baseQIndex = qIndex;
}
/// <summary>
/// Writes the frame-local intra-block-copy flag.
/// </summary>
/// <param name="value">Indicates whether intra-block copy is selected.</param>
public void WriteUseIntraBlockCopy(bool value)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol(value, this.tileIntraBlockCopy);
}
/// <summary>
/// Writes an integer intra-block-copy displacement vector relative to a spatial reference.
/// </summary>
/// <param name="value">The displacement vector to encode.</param>
/// <param name="reference">The spatially derived reference vector.</param>
public void WriteDisplacementVector(Av1MotionVector value, Av1MotionVector reference)
=> this.displacementVector.Write(this.writer, value, reference);
/// <summary>
/// Writes a complete block partition type using the selected partition context.
/// </summary>
/// <param name="partitionType">The partition type to encode.</param>
/// <param name="context">The partition probability context.</param>
public void WritePartitionType(Av1PartitionType partitionType, int context)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol((int)partitionType, this.tilePartitionTypes[context]);
}
/// <summary>
/// Writes the split-versus-horizontal boundary decision for a block clipped at the bottom tile edge.
/// </summary>
/// <param name="partitionType">The split or horizontal partition outcome.</param>
/// <param name="blockSize">The current block size.</param>
/// <param name="context">The partition probability context.</param>
public void WriteSplitOrHorizontal(Av1PartitionType partitionType, Av1BlockSize blockSize, int context)
{
uint frequency = Av1SymbolDecoder.GetSplitOrHorizontalFrequency(this.tilePartitionTypes, blockSize, context);
bool value = partitionType == Av1PartitionType.Split;
ref Av1SymbolWriter w = ref this.writer;
w.WriteBoolean(value, frequency);
}
/// <summary>
/// Writes the split-versus-vertical boundary decision for a block clipped at the right tile edge.
/// </summary>
/// <param name="partitionType">The split or vertical partition outcome.</param>
/// <param name="blockSize">The current block size.</param>
/// <param name="context">The partition probability context.</param>
public void WriteSplitOrVertical(Av1PartitionType partitionType, Av1BlockSize blockSize, int context)
{
uint frequency = Av1SymbolDecoder.GetSplitOrVerticalFrequency(this.tilePartitionTypes, blockSize, context);
bool value = partitionType == Av1PartitionType.Split;
ref Av1SymbolWriter w = ref this.writer;
w.WriteBoolean(value, frequency);
}
/// <summary>
/// Encodes one transform block's coefficient syntax using scan-order probability contexts.
/// </summary>
/// <param name="transformSize">The signaled transform size.</param>
/// <param name="transformType">The transform type selecting the scan and context class.</param>
/// <param name="intraDirection">The block's intra prediction mode.</param>
/// <param name="coefficientBuffer">The raster-ordered signed coefficient levels.</param>
/// <param name="componentType">The luma or chroma component category.</param>
/// <param name="transformBlockContext">The neighboring skip and DC sign contexts.</param>
/// <param name="endOfBlock">The one-based final nonzero scan position, or zero for an empty block.</param>
/// <param name="useReducedTransformSet">Indicates whether the frame restricts transform choices.</param>
/// <param name="filterIntraMode">The selected filter-intra mode, or the disabled sentinel.</param>
/// <returns>The packed coefficient context used by adjacent transform blocks.</returns>
public int WriteCoefficients(
Av1TransformSize transformSize,
Av1TransformType transformType,
Av1PredictionMode intraDirection,
Span<int> coefficientBuffer,
Av1ComponentType componentType,
Av1TransformBlockContext transformBlockContext,
ushort endOfBlock,
bool useReducedTransformSet,
Av1FilterIntraMode filterIntraMode)
{
int c;
Av1TransformSize adjustedTransformSize = transformSize.GetAdjusted();
int width = adjustedTransformSize.GetWidth();
int height = adjustedTransformSize.GetHeight();
Av1TransformClass transformClass = transformType.ToClass();
Av1ScanOrder scanOrder = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType);
ReadOnlySpan<short> scan = scanOrder.Scan;
Av1TransformSize transformSizeContext = Av1SymbolContextHelper.GetTransformSizeContext(transformSize);
ref Av1SymbolWriter w = ref this.writer;
// AV1 omits high-frequency coefficients beyond 32 samples on every 64-point transform dimension.
using Av1LevelBuffer levels = new(this.configuration, new Size(width, height));
Span<sbyte> coefficientContexts = new sbyte[width * height];
Guard.MustBeLessThan((int)transformSizeContext, (int)Av1TransformSize.AllSizes, nameof(transformSizeContext));
this.WriteTransformBlockSkip(endOfBlock == 0, transformSizeContext, transformBlockContext.SkipContext);
if (endOfBlock == 0)
{
return 0;
}
levels.Initialize(coefficientBuffer);
if (componentType == Av1ComponentType.Luminance)
{
this.WriteTransformType(transformType, transformSize, useReducedTransformSet, this.baseQIndex, filterIntraMode, intraDirection);
}
this.WriteEndOfBlockPosition(endOfBlock, componentType, transformClass, transformSize, transformSizeContext);
Av1SymbolContextHelper.GetNzMapContexts(levels, scan, endOfBlock, transformSize, transformClass, coefficientContexts);
int limitedTransformSizeContext = Math.Min((int)transformSizeContext, (int)Av1TransformSize.Size32x32);
for (c = endOfBlock - 1; c >= 0; --c)
{
short pos = scan[c];
int v = coefficientBuffer[pos];
short coeffContext = coefficientContexts[pos];
Point position = levels.GetPosition(pos);
int level = Math.Abs(v);
if (c == endOfBlock - 1)
{
w.WriteSymbol(Math.Min(level, 3) - 1, this.coefficientsBaseEndOfBlock[(int)transformSizeContext][(int)componentType][coeffContext]);
}
else
{
w.WriteSymbol(Math.Min(level, 3), this.coefficientsBase[(int)transformSizeContext][(int)componentType][coeffContext]);
}
if (level > Av1Constants.BaseLevelsCount)
{
// Base-range symbols extend levels above the two base levels in fixed-size chunks.
int baseRange = level - 1 - Av1Constants.BaseLevelsCount;
int baseRangeContext = Av1SymbolContextHelper.GetBaseRangeContext(levels, position, transformClass);
for (int idx = 0; idx < Av1Constants.CoefficientBaseRange; idx += Av1Constants.BaseRangeSizeMinus1)
{
int k = Math.Min(baseRange - idx, Av1Constants.BaseRangeSizeMinus1);
w.WriteSymbol(k, this.coefficientsBaseRange[limitedTransformSizeContext][(int)componentType][baseRangeContext]);
if (k < Av1Constants.BaseRangeSizeMinus1)
{
break;
}
}
}
}
// Signs follow every magnitude so the DC sign can use its neighboring context and AC signs remain literals.
int culLevel = 0;
for (c = 0; c < endOfBlock; ++c)
{
short pos = scan[c];
int v = coefficientBuffer[pos];
int level = Math.Abs(v);
culLevel += level;
uint sign = v < 0 ? 1u : 0u;
if (level > 0)
{
if (c == 0)
{
w.WriteSymbol((int)sign, this.dcSign[(int)componentType][transformBlockContext.DcSignContext]);
}
else
{
w.WriteLiteral(sign, 1);
}
if (level > (Av1Constants.CoefficientBaseRange + Av1Constants.BaseLevelsCount))
{
this.WriteGolomb(level - Av1Constants.CoefficientBaseRange - 1 - Av1Constants.BaseLevelsCount);
}
}
}
culLevel = Math.Min(Av1Constants.CoefficientContextMask, culLevel);
// The DC sign is packed above the magnitude bits so adjacent blocks can derive both contexts from one value.
Av1SymbolContextHelper.SetDcSign(ref culLevel, coefficientBuffer[0]);
return culLevel;
}
/// <summary>
/// Writes an end-of-block token and its context-coded and literal suffix bits.
/// </summary>
/// <param name="endOfBlock">The one-based final nonzero scan position.</param>
/// <param name="componentType">The luma or chroma component category.</param>
/// <param name="transformClass">The transform direction class.</param>
/// <param name="transformSize">The signaled transform size selecting the token alphabet.</param>
/// <param name="transformSizeContext">The square transform-size probability context.</param>
public void WriteEndOfBlockPosition(ushort endOfBlock, Av1ComponentType componentType, Av1TransformClass transformClass, Av1TransformSize transformSize, Av1TransformSize transformSizeContext)
{
short endOfBlockPosition = Av1SymbolContextHelper.GetEndOfBlockPosition(endOfBlock, out int eobExtra);
this.WriteEndOfBlockFlag(componentType, transformClass, transformSize, endOfBlockPosition);
int eobOffsetBitCount = Av1SymbolContextHelper.EndOfBlockOffsetBits[endOfBlockPosition];
if (eobOffsetBitCount > 0)
{
ref Av1SymbolWriter w = ref this.writer;
int eobShift = eobOffsetBitCount - 1;
int bit = Av1Math.GetBit(eobExtra, eobShift);
// The local table retains placeholders for the first three tokens, unlike the reference decoder's compact table,
// so the encoded token is also the distribution index.
int endOfBlockContext = endOfBlockPosition;
w.WriteSymbol(bit, this.endOfBlockExtra[(int)transformSizeContext][(int)componentType][endOfBlockContext]);
for (int i = 1; i < eobOffsetBitCount; i++)
{
eobShift = eobOffsetBitCount - 1 - i;
bit = Av1Math.GetBit(eobExtra, eobShift);
w.WriteLiteral((uint)bit, 1);
}
}
}
/// <summary>
/// Writes whether a transform block has no coded coefficients.
/// </summary>
/// <param name="skip">Indicates whether the transform block is empty.</param>
/// <param name="transformSizeContext">The square transform-size probability context.</param>
/// <param name="skipContext">The context derived from neighboring coefficient blocks.</param>
public void WriteTransformBlockSkip(bool skip, Av1TransformSize transformSizeContext, int skipContext)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol(skip, this.transformBlockSkip[(int)transformSizeContext][skipContext]);
}
/// <summary>
/// Finalizes the range-coded tile payload and transfers ownership of its memory.
/// </summary>
/// <returns>The memory owner containing the encoded tile bytes.</returns>
public IMemoryOwner<byte> Exit()
{
ref Av1SymbolWriter w = ref this.writer;
return w.Exit();
}
/// <summary>
/// Releases output memory that has not been transferred by <see cref="Exit"/>.
/// </summary>
public void Dispose()
{
if (!this.isDisposed)
{
this.writer.Dispose();
this.isDisposed = true;
}
}
/// <summary>
/// Writes the unsigned exponential-Golomb suffix used for coefficient levels beyond the base range.
/// </summary>
/// <param name="level">The nonnegative suffix value.</param>
public void WriteGolomb(int level)
{
uint x = (uint)level + 1u;
int length = (int)Av1Math.Log2_32(x) + 1;
Guard.MustBeGreaterThan(length, 0, nameof(length));
ref Av1SymbolWriter w = ref this.writer;
for (int i = 0; i < length - 1; ++i)
{
w.WriteLiteral(0u, 1);
}
for (int j = length - 1; j >= 0; --j)
{
w.WriteLiteral((x >> j) & 0x01, 1);
}
}
/// <summary>
/// Writes the end-of-block token for a transform coefficient-count category.
/// </summary>
/// <param name="componentType">The luma or chroma component category.</param>
/// <param name="transformClass">The transform direction class.</param>
/// <param name="transformSize">The signaled transform size.</param>
/// <param name="endOfBlockPosition">The one-based end-of-block token.</param>
private void WriteEndOfBlockFlag(Av1ComponentType componentType, Av1TransformClass transformClass, Av1TransformSize transformSize, int endOfBlockPosition)
{
int endOfBlockMultiSize = transformSize.GetLog2Minus4();
int endOfBlockContext = transformClass == Av1TransformClass.Class2D ? 0 : 1;
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol(endOfBlockPosition - 1, this.endOfBlockFlag[endOfBlockMultiSize][(int)componentType][endOfBlockContext]);
}
/// <summary>
/// Writes an intra transform type when the permitted transform set contains multiple choices.
/// </summary>
/// <param name="transformType">The transform type to encode.</param>
/// <param name="transformSize">The signaled transform size.</param>
/// <param name="useReducedTransformSet">Indicates whether the frame restricts transform choices.</param>
/// <param name="baseQIndex">The active base quantizer index.</param>
/// <param name="filterIntraMode">The filter-intra mode when enabled.</param>
/// <param name="intraDirection">The ordinary intra prediction mode.</param>
public void WriteTransformType(
Av1TransformType transformType,
Av1TransformSize transformSize,
bool useReducedTransformSet,
int baseQIndex,
Av1FilterIntraMode filterIntraMode,
Av1PredictionMode intraDirection)
{
// Still-image encoding reaches this path only for intra blocks, so the intra transform set is authoritative.
Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType(transformSize, useReducedTransformSet);
if (Av1SymbolContextHelper.GetExtendedTransformTypeCount(transformSetType) > 1 && baseQIndex > 0)
{
Av1TransformSize squareTransformSize = transformSize.GetSquareSize();
Guard.MustBeLessThanOrEqualTo((int)squareTransformSize, Av1Constants.ExtendedTransformCount, nameof(squareTransformSize));
int extendedSet = Av1SymbolContextHelper.GetExtendedTransformSet(transformSetType);
// Set zero contains only DCT-DCT, which was excluded by the multiple-choice condition above.
Guard.MustBeGreaterThan(extendedSet, 0, nameof(extendedSet));
Av1PredictionMode intraDirectionContext;
if (filterIntraMode != Av1FilterIntraMode.AllFilterIntraModes)
{
intraDirectionContext = filterIntraMode.ToIntraDirection();
}
else
{
intraDirectionContext = intraDirection;
}
Guard.MustBeLessThan((int)intraDirectionContext, 13, nameof(intraDirectionContext));
Guard.MustBeLessThan((int)squareTransformSize, 4, nameof(squareTransformSize));
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol(
Av1SymbolContextHelper.GetExtendedTransformIndex(transformSetType, transformType),
this.intraExtendedTransform[extendedSet][(int)squareTransformSize][(int)intraDirectionContext]);
}
}
/// <summary>
/// Writes a spatially predicted segment identifier.
/// </summary>
/// <param name="segmentId">The segment identifier.</param>
/// <param name="context">The context derived from neighboring segment identifiers.</param>
public void WriteSegmentId(int segmentId, int context)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol(segmentId, this.segmentId[context]);
}
/// <summary>
/// Writes the transform-skip flag from a neighboring skip context.
/// </summary>
/// <param name="skip">Indicates whether the block contains no coded transform coefficients.</param>
/// <param name="context">The neighboring skip context.</param>
public void WriteSkip(bool skip, int context)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol(skip, this.skip[context]);
}
/// <summary>
/// Writes the compound-reference skip-mode flag.
/// </summary>
/// <param name="skip">Indicates whether skip mode is selected.</param>
/// <param name="context">The neighboring skip-mode context.</param>
public void WriteSkipMode(bool skip, int context)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol(skip, this.skipMode[context]);
}
/// <summary>
/// Writes the filter-intra enable flag and, when enabled, its prediction mode.
/// </summary>
/// <param name="filterIntraMode">The selected filter-intra mode, or the disabled sentinel.</param>
/// <param name="blockSize">The block size selecting the enable distribution.</param>
public void WriteFilterIntraMode(Av1FilterIntraMode filterIntraMode, Av1BlockSize blockSize)
{
ref Av1SymbolWriter w = ref this.writer;
bool useFilter = filterIntraMode != Av1FilterIntraMode.AllFilterIntraModes;
w.WriteSymbol(useFilter, this.filterIntra[(int)blockSize]);
if (useFilter)
{
w.WriteSymbol((int)filterIntraMode, this.filterIntraMode);
}
}
/// <summary>
/// Writes a signed quantizer-index delta value.
/// </summary>
/// <param name="deltaQindex">The signed quantizer-index delta.</param>
public void WriteDeltaQuantizerIndex(int deltaQindex)
{
ref Av1SymbolWriter w = ref this.writer;
bool sign = deltaQindex < 0;
int abs = Math.Abs(deltaQindex);
bool isSmallValue = abs < Av1Constants.DeltaQuantizerSmall;
w.WriteSymbol(Math.Min(abs, Av1Constants.DeltaQuantizerSmall), this.deltaQuantizerAbsolute);
if (!isSmallValue)
{
// Escape magnitudes encode their bit width first, followed by the offset within that width's range.
int remainingBitCount = Av1Math.MostSignificantBit((uint)(abs - 1));
int threshold = (1 << remainingBitCount) + 1;
w.WriteLiteral((uint)(remainingBitCount - 1), 3);
w.WriteLiteral((uint)(abs - threshold), remainingBitCount);
}
if (abs > 0)
{
w.WriteLiteral(sign);
}
}
/// <summary>
/// Writes a key-frame luma prediction mode using the above and left mode contexts.
/// </summary>
/// <param name="lumaMode">The luma prediction mode.</param>
/// <param name="topContext">The reduced above-mode context.</param>
/// <param name="leftContext">The reduced left-mode context.</param>
public void WriteLumaMode(Av1PredictionMode lumaMode, byte topContext, byte leftContext)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol((int)lumaMode, this.keyFrameYMode[topContext][leftContext]);
}
/// <summary>
/// Writes an unsigned directional angle-delta symbol.
/// </summary>
/// <param name="angleDelta">The signed angle delta offset by <see cref="Av1Constants.MaxAngleDelta"/>.</param>
/// <param name="context">The directional prediction mode selecting the distribution.</param>
public void WriteAngleDelta(int angleDelta, Av1PredictionMode context)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol(angleDelta, this.angleDelta[context - Av1PredictionMode.Vertical]);
}
/// <summary>
/// Writes a fixed-width CDEF strength index.
/// </summary>
/// <param name="cdefStrength">The CDEF strength index.</param>
/// <param name="bitCount">The number of signaled bits.</param>
public void WriteCdefStrength(int cdefStrength, int bitCount)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteLiteral((uint)cdefStrength, bitCount);
}
/// <summary>
/// Writes a chroma intra prediction mode conditioned on the luma mode and chroma-from-luma availability.
/// </summary>
/// <param name="chromaMode">The chroma prediction mode.</param>
/// <param name="isChromaFromLumaAllowed">Indicates whether chroma-from-luma is valid for the block.</param>
/// <param name="lumaMode">The block's luma prediction mode.</param>
public void WriteChromaMode(Av1ChromaPredictionMode chromaMode, bool isChromaFromLumaAllowed, Av1PredictionMode lumaMode)
{
ref Av1SymbolWriter w = ref this.writer;
int cflAllowed = isChromaFromLumaAllowed ? 1 : 0;
w.WriteSymbol((int)chromaMode, this.uvMode[cflAllowed][(int)lumaMode]);
}
/// <summary>
/// Writes the joint chroma-from-luma signs and the magnitude index for each nonzero plane.
/// </summary>
/// <param name="chromaFromLumaIndex">The packed U/V alpha-magnitude indices.</param>
/// <param name="joinedSign">The joint U/V sign symbol.</param>
public void WriteChromaFromLumaAlphas(int chromaFromLumaIndex, int joinedSign)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol(joinedSign, this.chromaFromLumaSign);
// Magnitudes are only signaled for nonzero signs; the shared helper keeps encoder and decoder mappings exact.
int signU = Av1ChromaFromLumaMath.SignU(joinedSign);
if (signU != Av1ChromaFromLumaMath.SignZero)
{
int contextU = Av1ChromaFromLumaMath.ContextU(joinedSign);
int indexU = Av1ChromaFromLumaMath.IndexU(chromaFromLumaIndex);
w.WriteSymbol(indexU, this.chromaFromLumaAlpha[contextU]);
}
int signV = Av1ChromaFromLumaMath.SignV(joinedSign);
if (signV != Av1ChromaFromLumaMath.SignZero)
{
int contextV = Av1ChromaFromLumaMath.ContextV(joinedSign);
int indexV = Av1ChromaFromLumaMath.IndexV(chromaFromLumaIndex);
w.WriteSymbol(indexV, this.chromaFromLumaAlpha[contextV]);
}
}
}

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

@ -0,0 +1,290 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Reads AV1 literals and adaptively coded symbols from one bounded entropy-coded byte span.
/// </summary>
internal ref struct Av1SymbolReader
{
/// <summary>
/// The number of bits in the range-decoder code-value window.
/// </summary>
private const int DecoderWindowsSize = 32;
/// <summary>
/// The synthetic count used after the bounded input has been exhausted and zero padding begins.
/// </summary>
private const int LotsOfBits = 0x4000;
/// <summary>
/// The bounded entropy-coded bytes available to this reader.
/// </summary>
private readonly Span<byte> buffer;
/// <summary>
/// Indicates whether decoded symbols adapt their distributions.
/// </summary>
private readonly bool updateCdf;
/// <summary>
/// The next byte position to load into the code-value window.
/// </summary>
private int position;
/// <summary>
/// The difference between the upper end of the current range and the coded value, minus one.
/// </summary>
/// <remarks>
/// The decoder compares the upper 16 bits. Renormalization shifts consumed bits out and refills the lower portion
/// from <see cref="buffer"/> so the comparison remains aligned with <see cref="range"/>.
/// </remarks>
private uint difference;
/// <summary>
/// The number of code values in the current normalized interval.
/// </summary>
private uint range;
/// <summary>
/// The number of buffered bits below the 16-bit comparison window.
/// </summary>
private int count;
/// <summary>
/// The adjustment that preserves the logical consumed-bit count after the reader enters implicit zero padding.
/// </summary>
private int tellOffset;
/// <summary>
/// Initializes a new instance of the <see cref="Av1SymbolReader"/> struct over one entropy-coded span.
/// </summary>
/// <param name="span">The bounded entropy-coded bytes.</param>
/// <param name="updateCdf">A value indicating whether decoded symbols adapt their distributions.</param>
public Av1SymbolReader(Span<byte> span, bool updateCdf = true)
{
this.buffer = span;
this.updateCdf = updateCdf;
this.position = 0;
this.difference = (1U << (DecoderWindowsSize - 1)) - 1;
this.range = 0x8000;
this.count = -15;
this.tellOffset = 10 - (DecoderWindowsSize - 8);
this.Refill();
}
/// <summary>
/// Validates that range decoding remained within the bounded tile payload and ended at the required trailing-one bit.
/// </summary>
public void ValidateTrailingBits()
{
int consumedBitCount = this.GetConsumedBitCount();
int consumedByteCount = (consumedBitCount + 7) >> 3;
if (consumedByteCount > this.buffer.Length)
{
throw new InvalidImageContentException("The AV1 tile entropy stream is truncated.");
}
// The final consumed byte must contain one trailing-one bit at the range decoder's exact stopping position,
// followed only by zero bits. This is the same bounded-stream check performed after the reference decoder decodes a tile.
int trailingOneBit = 128 >> ((consumedBitCount - 1) & 7);
int trailingBitMask = (trailingOneBit << 1) - 1;
if ((this.buffer[consumedByteCount - 1] & trailingBitMask) != trailingOneBit)
{
throw new InvalidImageContentException("The AV1 tile entropy stream has invalid trailing bits.");
}
for (int i = consumedByteCount; i < this.buffer.Length; i++)
{
if (this.buffer[i] != 0)
{
throw new InvalidImageContentException("The AV1 tile entropy stream has nonzero padding bytes.");
}
}
}
/// <summary>
/// Reads one symbol and adapts its distribution when CDF updates are enabled.
/// </summary>
/// <param name="distribution">The inverse cumulative distribution for the symbol alphabet.</param>
/// <returns>The decoded zero-based symbol.</returns>
public int ReadSymbol(Av1Distribution distribution)
{
int value = this.DecodeIntegerQ15(distribution);
// disable_cdf_update freezes every tile distribution while leaving range decoding unchanged.
if (this.updateCdf)
{
distribution.Update(value);
}
return value;
}
/// <summary>
/// Reads one non-adaptive binary symbol using the supplied Q15 probability for <see langword="true"/>.
/// </summary>
/// <param name="frequency">The probability that the symbol is <see langword="true"/>, scaled by 32768.</param>
/// <returns>The decoded binary symbol.</returns>
public bool ReadBoolean(uint frequency) => this.DecodeBoolQ15(frequency);
/// <summary>
/// Reads an unsigned literal in most-significant-bit-first order.
/// </summary>
/// <param name="bitCount">The number of literal bits to read.</param>
/// <returns>The decoded literal.</returns>
public int ReadLiteral(int bitCount)
{
const uint prob = (0x7FFFFFU - (128 << 15) + 128) >> 8;
int literal = 0;
for (int bit = bitCount - 1; bit >= 0; bit--)
{
if (this.DecodeBoolQ15(prob))
{
literal |= 1 << bit;
}
}
return literal;
}
/// <summary>
/// Decode a single binary value.
/// </summary>
/// <param name="frequency">The probability that the bit is one, scaled by 32768.</param>
/// <returns>The decoded binary value.</returns>
private bool DecodeBoolQ15(uint frequency)
{
uint dif;
uint vw;
uint range;
uint newRange;
uint v;
bool ret;
dif = this.difference;
range = this.range;
// Reserve a minimum interval for both outcomes after reducing the Q15 frequency to the range-coder
// multiplication precision. This is the same rounding model used by Av1SymbolWriter.
v = ((range >> 8) * (frequency >> Av1Distribution.ProbabilityShift)) >> (7 - Av1Distribution.ProbabilityShift);
v += Av1Distribution.ProbabilityMinimum;
vw = v << (DecoderWindowsSize - 16);
ret = true;
newRange = v;
if (dif >= vw)
{
newRange = range - v;
dif -= vw;
ret = false;
}
this.Normalize(dif, newRange);
return ret;
}
/// <summary>
/// Decodes a symbol given an inverse cumulative distribution function(CDF) table in Q15.
/// </summary>
/// <param name="distribution">
/// CDF_PROB_TOP minus the CDF, such that symbol s falls in the range
/// [s > 0 ? (CDF_PROB_TOP - icdf[s - 1]) : 0, CDF_PROB_TOP - icdf[s]).
/// The values must be monotonically non - increasing, and icdf[nsyms - 1] must be 0.
/// </param>
/// <returns>The decoded symbol.</returns>
private int DecodeIntegerQ15(Av1Distribution distribution)
{
uint c;
uint u;
uint v;
int ret;
uint dif = this.difference;
uint r = this.range;
int n = distribution.NumberOfSymbols - 1;
DebugGuard.MustBeLessThan(dif >> (DecoderWindowsSize - 16), r, nameof(r));
DebugGuard.IsTrue(distribution[n] == 0, "Last value in probability array needs to be zero.");
DebugGuard.MustBeGreaterThanOrEqualTo(r, 32768U, nameof(r));
DebugGuard.MustBeGreaterThanOrEqualTo(7 - Av1Distribution.ProbabilityShift - Av1Distribution.CdfShift, 0, nameof(Av1Distribution.CdfShift));
c = dif >> (DecoderWindowsSize - 16);
v = r;
ret = -1;
do
{
u = v;
v = ((r >> 8) * (distribution[++ret] >> Av1Distribution.ProbabilityShift)) >> (7 - Av1Distribution.ProbabilityShift - Av1Distribution.CdfShift);
v += (uint)(Av1Distribution.ProbabilityMinimum * (n - ret));
}
while (c < v);
DebugGuard.MustBeLessThan(v, u, nameof(v));
DebugGuard.MustBeLessThanOrEqualTo(u, r, nameof(u));
r = u - v;
dif -= v << (DecoderWindowsSize - 16);
this.Normalize(dif, r);
return ret;
}
/// <summary>
/// Takes updated dif and range values, renormalizes them so that
/// <paramref name="rng"/> has value between 32768 and 65536 (reading more bytes from the stream into dif if
/// necessary), and stores them back in the decoder context.
/// </summary>
/// <param name="dif">The updated code-value difference.</param>
/// <param name="rng">The updated coding interval width.</param>
private void Normalize(uint dif, uint rng)
{
// Shifting by the leading-zero count restores the interval to [32768, 65536) and consumes the same number of
// code-value bits. Adding one before the shift preserves the decoder's difference-minus-one representation.
int d = 15 - Av1Math.MostSignificantBit(rng);
this.count -= d;
this.difference = ((dif + 1) << d) - 1;
this.range = rng << d;
if (this.count < 0)
{
this.Refill();
}
}
/// <summary>
/// Loads whole bytes into the lower portion of the code-value window after renormalization.
/// </summary>
private void Refill()
{
uint dif = this.difference;
int cnt = this.count;
int position = this.position;
int end = this.buffer.Length;
int s = DecoderWindowsSize - 9 - (cnt + 15);
for (; s >= 0 && position < end; s -= 8, position++)
{
// XOR inserts a source byte into the difference-minus-one representation. Advancing both the byte
// position and buffered-bit count leaves the logical number of consumed bits unchanged.
DebugGuard.MustBeLessThan(s, DecoderWindowsSize - 8, nameof(s));
dif ^= (uint)this.buffer[position] << s;
cnt += 8;
}
if (position >= end)
{
// AV1 range decoding permits the final interval to consume implicit zero padding. A large count models
// that padding without repeatedly attempting to refill it. Preserve the previous count in tellOffset so
// the logical position continues past the bounded source and truncated payloads remain detectable.
this.tellOffset += LotsOfBits - cnt;
cnt = LotsOfBits;
}
this.difference = dif;
this.count = cnt;
this.position = position;
}
/// <summary>
/// Gets the number of entropy bits consumed from the bounded tile payload, including the initial range-coder bit.
/// </summary>
/// <returns>The logical consumed-bit count.</returns>
private readonly int GetConsumedBitCount()
=> (this.position * 8) - this.count + this.tellOffset;
}

302
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs

@ -0,0 +1,302 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary>
/// Writes AV1 literals and adaptively coded symbols to a range-coded byte sequence.
/// </summary>
internal class Av1SymbolWriter : IDisposable
{
/// <summary>
/// The lower endpoint of the current coding interval.
/// </summary>
private uint low;
/// <summary>
/// The width of the current normalized coding interval.
/// </summary>
private uint rng = 0x8000U;
/// <summary>
/// The number of accumulated bits relative to the next byte-and-carry flush boundary.
/// </summary>
/// <remarks>
/// The initial value of -9 crosses zero after one output byte and its carry bit have accumulated.
/// </remarks>
private int cnt = -9;
/// <summary>
/// The configuration that supplies output allocation.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// The pre-carry output values accumulated during renormalization.
/// </summary>
private readonly AutoExpandingMemory<ushort> memory;
/// <summary>
/// Indicates whether encoded symbols adapt their distributions.
/// </summary>
private readonly bool updateCdf;
/// <summary>
/// The next pre-carry output position.
/// </summary>
private int position;
/// <summary>
/// Initializes a new instance of the <see cref="Av1SymbolWriter"/> class with an estimated output size.
/// </summary>
/// <param name="configuration">The configuration that supplies output allocation.</param>
/// <param name="initialSize">The estimated encoded size in bytes.</param>
/// <param name="updateCdf">A value indicating whether encoded symbols adapt their distributions.</param>
public Av1SymbolWriter(Configuration configuration, int initialSize, bool updateCdf = true)
{
this.configuration = configuration;
this.memory = new AutoExpandingMemory<ushort>(configuration, (initialSize + 1) >> 1);
this.updateCdf = updateCdf;
}
/// <summary>
/// Releases the expandable pre-carry buffer.
/// </summary>
public void Dispose() => this.memory.Dispose();
/// <summary>
/// Writes one binary symbol and adapts its distribution when CDF updates are enabled.
/// </summary>
/// <param name="symbol">The binary symbol.</param>
/// <param name="distribution">The inverse cumulative distribution for the binary alphabet.</param>
public void WriteSymbol(bool symbol, Av1Distribution distribution)
=> this.WriteSymbol(symbol ? 1 : 0, distribution);
/// <summary>
/// Writes one symbol and adapts its distribution when CDF updates are enabled.
/// </summary>
/// <param name="symbol">The zero-based symbol.</param>
/// <param name="distribution">The inverse cumulative distribution for the symbol alphabet.</param>
public void WriteSymbol(int symbol, Av1Distribution distribution)
{
DebugGuard.MustBeGreaterThanOrEqualTo(symbol, 0, nameof(symbol));
DebugGuard.MustBeLessThan(symbol, distribution.NumberOfSymbols, nameof(symbol));
DebugGuard.IsTrue(distribution[distribution.NumberOfSymbols - 1] == 0, "Last entry in Probabilities table needs to be zero.");
this.EncodeIntegerQ15(symbol, distribution);
// disable_cdf_update freezes every tile distribution while leaving range encoding unchanged.
if (this.updateCdf)
{
distribution.Update(symbol);
}
}
/// <summary>
/// Writes one non-adaptive binary symbol using the supplied Q15 probability for <see langword="true"/>.
/// </summary>
/// <param name="value">The binary symbol.</param>
/// <param name="frequency">The probability that the symbol is <see langword="true"/>, scaled by 32768.</param>
public void WriteBoolean(bool value, uint frequency) => this.EncodeBoolQ15(value, frequency);
/// <summary>
/// Writes one equiprobable literal bit.
/// </summary>
/// <param name="value">The literal bit.</param>
public void WriteLiteral(bool value) => this.WriteLiteral(value ? 1u : 0u, 1);
/// <summary>
/// Writes the requested low-order bits in most-significant-bit-first order.
/// </summary>
/// <param name="value">The unsigned literal value.</param>
/// <param name="bitCount">The number of low-order bits to write.</param>
public void WriteLiteral(uint value, int bitCount)
{
const uint p = 0x4000U; // (0x7FFFFFU - (128 << 15) + 128) >> 8;
for (int bit = bitCount - 1; bit >= 0; bit--)
{
bool bitValue = ((value >> bit) & 0x1) > 0;
this.EncodeBoolQ15(bitValue, p);
}
}
/// <summary>
/// Terminates the range-coded sequence and propagates pending carries into an owned byte buffer.
/// </summary>
/// <returns>An owner containing the shortest byte sequence that preserves every encoded symbol.</returns>
public IMemoryOwner<byte> Exit()
{
// Round the low endpoint into the current interval so the emitted prefix selects every symbol encoded so far
// regardless of the bits that follow it.
uint l = this.low;
int c = this.cnt;
int pos = this.position;
int s = 10;
uint m = 0x3FFFU;
uint e = ((l + m) & ~m) | (m + 1);
s += c;
Span<ushort> buffer = this.memory.GetSpan(this.position + ((s + 7) >> 3));
if (s > 0)
{
uint n = (1U << (c + 16)) - 1;
do
{
buffer[pos] = (ushort)(e >> (c + 16));
pos++;
e &= n;
s -= 8;
c -= 8;
n >>= 8;
}
while (s > 0);
}
c = Math.Max((s + 7) >> 3, 0);
IMemoryOwner<byte> output = this.configuration.MemoryAllocator.Allocate<byte>(pos + c);
// Pre-carry values use 16-bit elements so a byte plus a propagated carry can coexist. Walking backwards folds
// each carry into the preceding byte without shifting the buffered sequence.
Span<byte> outputSlice = output.GetSpan()[(output.Length() - pos)..];
c = 0;
while (pos > 0)
{
pos--;
c = buffer[pos] + c;
outputSlice[pos] = (byte)c;
c >>= 8;
}
return output;
}
/// <summary>
/// Encode a single binary value.
/// </summary>
/// <param name="val">The value to encode.</param>
/// <param name="frequency">The probability that the value is true, scaled by 32768.</param>
private void EncodeBoolQ15(bool val, uint frequency)
{
uint l;
uint r;
uint v;
DebugGuard.MustBeGreaterThan(frequency, 0U, nameof(frequency));
DebugGuard.MustBeLessThanOrEqualTo(frequency, 32768U, nameof(frequency));
l = this.low;
r = this.rng;
DebugGuard.MustBeGreaterThanOrEqualTo(r, 32768U, nameof(r));
// Reduce the Q15 frequency to the range-coder multiplication precision and retain a nonzero interval for
// both outcomes. Av1SymbolReader applies the identical rounding model.
v = ((r >> 8) * (frequency >> Av1Distribution.ProbabilityShift)) >> (7 - Av1Distribution.ProbabilityShift);
v += Av1Distribution.ProbabilityMinimum;
if (val)
{
l += r - v;
r = v;
}
else
{
r -= v;
}
this.Normalize(l, r);
}
/// <summary>
/// Encodes a symbol given an inverse cumulative distribution function(CDF) table in Q15.
/// </summary>
/// <param name="symbol">The value to encode.</param>
/// <param name="distribution">
/// CDF_PROB_TOP minus the CDF, such that symbol s falls in the range
/// [s > 0 ? (CDF_PROB_TOP - icdf[s - 1]) : 0, CDF_PROB_TOP - icdf[s]).
/// The values must be monotonically non - increasing, and icdf[nsyms - 1] must be 0.
/// </param>
private void EncodeIntegerQ15(int symbol, Av1Distribution distribution)
=> this.EncodeIntegerQ15(symbol > 0 ? distribution[symbol - 1] : Av1Distribution.ProbabilityTop, distribution[symbol], symbol, distribution.NumberOfSymbols);
/// <summary>
/// Narrows the coding interval to one symbol's inverse-cumulative bounds.
/// </summary>
/// <param name="lowFrequency">The inverse cumulative threshold preceding the symbol.</param>
/// <param name="highFrequency">The inverse cumulative threshold following the symbol.</param>
/// <param name="symbol">The zero-based symbol.</param>
/// <param name="numberOfSymbols">The size of the symbol alphabet.</param>
private void EncodeIntegerQ15(uint lowFrequency, uint highFrequency, int symbol, int numberOfSymbols)
{
const int totalShift = 7 - Av1Distribution.ProbabilityShift - Av1Distribution.CdfShift;
uint l = this.low;
uint r = this.rng;
DebugGuard.MustBeLessThanOrEqualTo(32768U, r, nameof(r));
DebugGuard.MustBeLessThanOrEqualTo(highFrequency, lowFrequency, nameof(highFrequency));
DebugGuard.MustBeLessThanOrEqualTo(lowFrequency, 32768U, nameof(lowFrequency));
DebugGuard.MustBeGreaterThanOrEqualTo(totalShift, 0, nameof(totalShift));
int n = numberOfSymbols - 1;
if (lowFrequency < Av1Distribution.ProbabilityTop)
{
uint u;
uint v;
u = (uint)((((r >> 8) * (lowFrequency >> Av1Distribution.ProbabilityShift)) >> totalShift) +
(Av1Distribution.ProbabilityMinimum * (n - (symbol - 1))));
v = (uint)((((r >> 8) * (highFrequency >> Av1Distribution.ProbabilityShift)) >> totalShift) +
(Av1Distribution.ProbabilityMinimum * (n - symbol)));
l += r - u;
r = u - v;
}
else
{
r -= (uint)((((r >> 8) * (highFrequency >> Av1Distribution.ProbabilityShift)) >> totalShift) +
(Av1Distribution.ProbabilityMinimum * (n - symbol)));
}
this.Normalize(l, r);
}
/// <summary>
/// Takes updated low and range values, renormalizes them so that <paramref name="rng"/>
/// lies between 32768 and 65536 (flushing bytes from low to the pre-carry buffer if necessary),
/// and stores them back in the encoder context.
/// </summary>
/// <param name="low">The new value of <see cref="low"/>.</param>
/// <param name="rng">The new value of <see cref="rng"/>.</param>
private void Normalize(uint low, uint rng)
{
int d;
int c;
int s;
c = this.cnt;
DebugGuard.MustBeLessThanOrEqualTo(rng, 65535U, nameof(rng));
d = 15 - Av1Math.MostSignificantBit(rng);
s = c + d;
// The 32-bit low endpoint is flushed whenever a byte becomes available. Retaining pre-carry values as
// ushort elements defers carry propagation until Exit without requiring a separate wider coding window.
if (s >= 0)
{
uint m;
Span<ushort> buffer = this.memory.GetSpan(this.position + 2);
c += 16;
m = (1U << c) - 1;
if (s >= 8)
{
buffer[this.position] = (ushort)(low >> c);
this.position++;
low &= m;
c -= 8;
m >>= 8;
}
buffer[this.position] = (ushort)(low >> c);
this.position++;
s = c + d - 24;
low &= m;
}
this.low = low << d;
this.rng = rng << d;
this.cnt = s;
}
}

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

@ -0,0 +1,22 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Defines tile-payload consumption and completion for one coded AV1 frame.
/// </summary>
internal interface IAv1TileReader
{
/// <summary>
/// Reads one entropy-coded tile payload into the current frame state.
/// </summary>
/// <param name="tileData">The bounded bitstream bytes belonging to the tile.</param>
/// <param name="tileNum">The zero-based tile index in raster order.</param>
void ReadTile(Span<byte> tileData, int tileNum);
/// <summary>
/// Completes the current coded frame after all tile payloads have been read and releases frame-scoped resources.
/// </summary>
void CompleteFrame();
}

19
src/ImageSharp/Formats/Heif/Av1/IAv1TileWriter.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Interface for writing of image tiles.
/// </summary>
internal interface IAv1TileWriter
{
/// <summary>
/// Write the information for a single tile.
/// </summary>
/// <param name="tileNum">The index of the tile that is to be read.</param>
/// <returns>
/// The bytes of encoded data in the bitstream dedicated to this tile.
/// </returns>
Span<byte> WriteTile(int tileNum);
}

163
src/ImageSharp/Formats/Heif/Av1/ModeDecision/Av1BlockGeometry.cs

@ -0,0 +1,163 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.ModeDecision;
/// <summary>
/// Describes the spatial, chroma, and transform layout of one AV1 block considered by mode-decision scanning.
/// </summary>
internal class Av1BlockGeometry
{
/// <summary>
/// The luma block size from which the cached luma dimensions are derived.
/// </summary>
private Av1BlockSize blockSize;
/// <summary>
/// The chroma block size from which the cached chroma dimensions are derived.
/// </summary>
private Av1BlockSize blockSizeUv;
/// <summary>
/// Initializes a new instance of the <see cref="Av1BlockGeometry"/> class with storage for every supported transform depth.
/// </summary>
public Av1BlockGeometry()
{
this.RedunancyList = [];
this.TransformOrigin = new Point[Av1Constants.MaxVarTransform + 1][];
for (int i = 0; i < this.TransformOrigin.Length; i++)
{
this.TransformOrigin[i] = new Point[Av1Constants.MaxTransformBlockCount];
}
}
/// <summary>
/// Gets or sets the luma block size and updates <see cref="BlockWidth"/> and <see cref="BlockHeight"/> to match.
/// </summary>
public Av1BlockSize BlockSize
{
get => this.blockSize;
set
{
this.blockSize = value;
this.BlockWidth = value.GetWidth();
this.BlockHeight = value.GetHeight();
}
}
/// <summary>
/// Gets or sets the chroma block size and updates <see cref="BlockWidthUv"/> and <see cref="BlockHeightUv"/> to match.
/// </summary>
public Av1BlockSize BlockSizeUv
{
get => this.blockSizeUv;
set
{
this.blockSizeUv = value;
this.BlockWidthUv = value.GetWidth();
this.BlockHeightUv = value.GetHeight();
}
}
/// <summary>
/// Gets or sets the block origin in pixels relative to the top-left corner of its superblock.
/// </summary>
public Point Origin { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this luma block owns chroma samples in the mode-decision layout.
/// </summary>
public bool HasUv { get; set; }
/// <summary>
/// Gets the luma block width in pixels.
/// </summary>
public int BlockWidth { get; private set; }
/// <summary>
/// Gets the luma block height in pixels.
/// </summary>
public int BlockHeight { get; private set; }
/// <summary>
/// Gets the number of luma transform blocks at each transform depth.
/// </summary>
public int[] TransformBlockCount { get; } = new int[Av1Constants.MaxVarTransform + 1];
/// <summary>
/// Gets the luma transform size selected at each transform depth.
/// </summary>
public Av1TransformSize[] TransformSize { get; } = new Av1TransformSize[Av1Constants.MaxVarTransform + 1];
/// <summary>
/// Gets the chroma transform size selected at each transform depth.
/// </summary>
public Av1TransformSize[] TransformSizeUv { get; } = new Av1TransformSize[Av1Constants.MaxVarTransform + 1];
/// <summary>
/// Gets the pixel origins of the transform blocks at each transform depth.
/// </summary>
public Point[][] TransformOrigin { get; private set; }
/// <summary>
/// Gets or sets the block index in mode-decision scan order.
/// </summary>
public int ModeDecisionIndex { get; set; }
/// <summary>
/// Gets or sets the scan offset from this square block to the next block at the same depth.
/// </summary>
public int NextDepthOffset { get; set; }
/// <summary>
/// Gets or sets the scan offset from this square block to its first child at the next depth.
/// </summary>
public int Depth1Offset { get; set; }
/// <summary>
/// Gets a value indicating whether this block is redundant to another.
/// </summary>
public bool IsRedundant => this.RedunancyList.Count > 0;
/// <summary>
/// Gets or sets the mode-decision indices of blocks with the same size and origin as this block.
/// </summary>
public List<int> RedunancyList { get; set; }
/// <summary>
/// Gets or sets the zero-based component index of this block within a non-square partition.
/// </summary>
public int NonSquareIndex { get; set; }
/// <summary>
/// Gets or sets the number of component blocks produced by this partition shape.
/// </summary>
public int TotalNonSuareCount { get; set; }
/// <summary>
/// Gets the chroma block width in pixels.
/// </summary>
public int BlockWidthUv { get; private set; }
/// <summary>
/// Gets the chroma block height in pixels.
/// </summary>
public int BlockHeightUv { get; private set; }
/// <summary>
/// Gets or sets the quadtree depth of this block within its superblock.
/// </summary>
public int Depth { get; set; }
/// <summary>
/// Gets or sets the width and height, in pixels, of the square sequence region that produced this block.
/// </summary>
public int SequenceSize { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this block belongs to the last quadrant of its parent.
/// </summary>
public bool IsLastQuadrant { get; set; }
}

1050
src/ImageSharp/Formats/Heif/Av1/ModeDecision/Av1BlockGeometryFactory.cs

File diff suppressed because it is too large

50
src/ImageSharp/Formats/Heif/Av1/ModeDecision/Av1GeometryIndex.cs

@ -0,0 +1,50 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.ModeDecision;
/// <summary>
/// Identifies a predefined AV1 mode-decision geometry with a fixed superblock size, search depth, and partition set.
/// </summary>
internal enum Av1GeometryIndex
{
/// <summary>
/// The 64-pixel, four-depth geometry limited to square partitions.
/// </summary>
Geometry0,
/// <summary>
/// The 64-pixel, four-depth geometry with horizontal and vertical binary partitions down to 16 pixels.
/// </summary>
Geometry1,
/// <summary>
/// The 64-pixel, four-depth geometry with horizontal and vertical binary partitions down to 8 pixels.
/// </summary>
Geometry2,
/// <summary>
/// The 64-pixel, four-depth geometry with binary partitions at every supported size.
/// </summary>
Geometry3,
/// <summary>
/// The 64-pixel, five-depth geometry with binary partitions at every supported size.
/// </summary>
Geometry4,
/// <summary>
/// The 64-pixel, five-depth geometry that also enables four-way horizontal and vertical partitions.
/// </summary>
Geometry5,
/// <summary>
/// The 64-pixel, five-depth geometry that enables all supported partition shapes.
/// </summary>
Geometry6,
/// <summary>
/// The 128-pixel, six-depth geometry that enables all supported partition shapes.
/// </summary>
Geometry7,
}

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

@ -0,0 +1,500 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
/// <summary>
/// Stores one AV1 global-motion model in the codec's fixed-point affine matrix domain.
/// </summary>
internal struct Av1GlobalMotionParameters
{
/// <summary>
/// The number of fractional bits carried by every stored matrix parameter.
/// </summary>
public const int ModelPrecisionBits = 16;
/// <summary>
/// The fixed-point representation of one in the global-motion matrix domain.
/// </summary>
public const int ModelScale = 1 << ModelPrecisionBits;
/// <summary>
/// The number of low-order bits removed from the derived shear parameters.
/// </summary>
private const int ShearParameterReductionBits = 6;
/// <summary>
/// The number of fractional bits carried by entries in <see cref="ReciprocalTable"/>.
/// </summary>
private const int ReciprocalPrecisionBits = 14;
/// <summary>
/// The number of divisor-fraction bits used to index <see cref="ReciprocalTable"/>.
/// </summary>
private const int ReciprocalIndexBits = 8;
/// <summary>
/// The largest difference between a retained neighbor motion vector and the current block motion vector.
/// </summary>
private const int LocalProjectionMotionVectorLimit = 256;
/// <summary>
/// The maximum magnitude of a non-diagonal affine coefficient relative to the identity matrix.
/// </summary>
private const int NonDiagonalAffineClamp = 1 << (ModelPrecisionBits - 3);
/// <summary>
/// The exclusive upper magnitude of either translation coefficient.
/// </summary>
private const int TranslationClamp = 128 << ModelPrecisionBits;
/// <summary>
/// The six parameters ordered as horizontal translation, vertical translation, and the four affine coefficients.
/// </summary>
private InlineArray6<int> matrix;
/// <summary>
/// Gets an identity global-motion model.
/// </summary>
public static Av1GlobalMotionParameters Identity
{
get
{
Av1GlobalMotionParameters result = default;
result.matrix[2] = ModelScale;
result.matrix[5] = ModelScale;
return result;
}
}
/// <summary>
/// Gets or sets the geometric model represented by the matrix parameters.
/// </summary>
public Av1GlobalMotionType Type { get; set; }
/// <summary>
/// Gets the reduced horizontal scale delta used by warped prediction.
/// </summary>
public short Alpha { get; private set; }
/// <summary>
/// Gets the reduced horizontal shear used by warped prediction.
/// </summary>
public short Beta { get; private set; }
/// <summary>
/// Gets the reduced vertical shear used by warped prediction.
/// </summary>
public short Gamma { get; private set; }
/// <summary>
/// Gets the reduced vertical scale delta used by warped prediction.
/// </summary>
public short Delta { get; private set; }
/// <summary>
/// Gets a value indicating whether the affine model violates AV1's permitted shear bounds.
/// </summary>
public bool IsInvalid { get; private set; }
/// <summary>
/// Gets the fixed-point reciprocal lookup used by AV1's affine shear derivation.
/// </summary>
private static ReadOnlySpan<ushort> ReciprocalTable =>
[
16384, 16320, 16257, 16194, 16132, 16070, 16009, 15948, 15888, 15828, 15768,
15709, 15650, 15592, 15534, 15477, 15420, 15364, 15308, 15252, 15197, 15142,
15087, 15033, 14980, 14926, 14873, 14821, 14769, 14717, 14665, 14614, 14564,
14513, 14463, 14413, 14364, 14315, 14266, 14218, 14170, 14122, 14075, 14028,
13981, 13935, 13888, 13843, 13797, 13752, 13707, 13662, 13618, 13574, 13530,
13487, 13443, 13400, 13358, 13315, 13273, 13231, 13190, 13148, 13107, 13066,
13026, 12985, 12945, 12906, 12866, 12827, 12788, 12749, 12710, 12672, 12633,
12596, 12558, 12520, 12483, 12446, 12409, 12373, 12336, 12300, 12264, 12228,
12193, 12157, 12122, 12087, 12053, 12018, 11984, 11950, 11916, 11882, 11848,
11815, 11782, 11749, 11716, 11683, 11651, 11619, 11586, 11555, 11523, 11491,
11460, 11429, 11398, 11367, 11336, 11305, 11275, 11245, 11215, 11185, 11155,
11125, 11096, 11067, 11038, 11009, 10980, 10951, 10923, 10894, 10866, 10838,
10810, 10782, 10755, 10727, 10700, 10673, 10645, 10618, 10592, 10565, 10538,
10512, 10486, 10460, 10434, 10408, 10382, 10356, 10331, 10305, 10280, 10255,
10230, 10205, 10180, 10156, 10131, 10107, 10082, 10058, 10034, 10010, 9986,
9963, 9939, 9916, 9892, 9869, 9846, 9823, 9800, 9777, 9754, 9732, 9709, 9687,
9664, 9642, 9620, 9598, 9576, 9554, 9533, 9511, 9489, 9468, 9447, 9425, 9404,
9383, 9362, 9341, 9321, 9300, 9279, 9259, 9239, 9218, 9198, 9178, 9158, 9138,
9118, 9098, 9079, 9059, 9039, 9020, 9001, 8981, 8962, 8943, 8924, 8905, 8886,
8867, 8849, 8830, 8812, 8793, 8775, 8756, 8738, 8720, 8702, 8684, 8666, 8648,
8630, 8613, 8595, 8577, 8560, 8542, 8525, 8508, 8490, 8473, 8456, 8439, 8422,
8405, 8389, 8372, 8355, 8339, 8322, 8306, 8289, 8273, 8257, 8240, 8224, 8208,
8192,
];
/// <summary>
/// Gets or sets a matrix parameter in AV1 affine-transform order.
/// </summary>
/// <param name="index">The zero-based matrix parameter index.</param>
/// <returns>The fixed-point matrix parameter.</returns>
public int this[int index]
{
get => this.matrix[index];
set => this.matrix[index] = value;
}
/// <summary>
/// Gets the translational motion vector represented by this model at the center of a coding block.
/// </summary>
/// <param name="allowHighPrecisionMotionVector">
/// A value indicating whether motion vectors may retain one-eighth-sample precision.
/// </param>
/// <param name="blockSize">The coding block size.</param>
/// <param name="modeInfoPosition">The block origin in 4x4 mode-information units.</param>
/// <param name="forceIntegerMotionVector">
/// A value indicating whether the result is rounded to an integer-sample displacement.
/// </param>
/// <returns>The global motion vector in one-eighth-sample units.</returns>
public readonly Av1MotionVector GetMotionVector(
bool allowHighPrecisionMotionVector,
Av1BlockSize blockSize,
Point modeInfoPosition,
bool forceIntegerMotionVector)
{
if (this.Type == Av1GlobalMotionType.Identity)
{
return default;
}
int row;
int column;
if (this.Type == Av1GlobalMotionType.Translation)
{
// AV1 accidentally assigns the horizontal translation parameter to the row component and the vertical
// parameter to the column component. Decoders preserve that published bitstream behavior for conformance.
row = this.matrix[0] >> (ModelPrecisionBits - 3);
column = this.matrix[1] >> (ModelPrecisionBits - 3);
}
else
{
int blockCenterX = (modeInfoPosition.X << Av1Constants.ModeInfoSizeLog2) + (blockSize.GetWidth() >> 1) - 1;
int blockCenterY = (modeInfoPosition.Y << Av1Constants.ModeInfoSizeLog2) + (blockSize.GetHeight() >> 1) - 1;
int horizontal = ((this.matrix[2] - ModelScale) * blockCenterX) +
(this.matrix[3] * blockCenterY) +
this.matrix[0];
int vertical = (this.matrix[4] * blockCenterX) +
((this.matrix[5] - ModelScale) * blockCenterY) +
this.matrix[1];
int precisionBits = allowHighPrecisionMotionVector ? ModelPrecisionBits - 3 : ModelPrecisionBits - 2;
column = Av1Math.RoundPowerOf2Signed(horizontal, precisionBits);
row = Av1Math.RoundPowerOf2Signed(vertical, precisionBits);
if (!allowHighPrecisionMotionVector)
{
column *= 2;
row *= 2;
}
}
return new Av1MotionVector(row, column).LowerPrecision(
allowHighPrecision: allowHighPrecisionMotionVector,
forceInteger: forceIntegerMotionVector);
}
/// <summary>
/// Derives the local affine model for a warped inter block from its spatial neighbor samples.
/// </summary>
/// <param name="sourcePoints">The neighbor-center positions relative to the current block in one-eighth-sample units.</param>
/// <param name="referencePoints">The corresponding positions in the selected reference frame.</param>
/// <param name="blockSize">The current coding block size.</param>
/// <param name="motionVector">The current block motion vector in one-eighth-sample units.</param>
/// <param name="modeInfoPosition">The current block origin in 4x4 mode-information units.</param>
/// <returns>The derived affine model, marked invalid when AV1's projection or shear constraints cannot be satisfied.</returns>
public static Av1GlobalMotionParameters DeriveLocalProjection(
ReadOnlySpan<Point> sourcePoints,
ReadOnlySpan<Point> referencePoints,
Av1BlockSize blockSize,
Av1MotionVector motionVector,
Point modeInfoPosition)
{
Av1GlobalMotionParameters result = Identity;
result.Type = Av1GlobalMotionType.Affine;
int blockWidth = blockSize.GetWidth();
int blockHeight = blockSize.GetHeight();
int sampleThreshold = Math.Clamp(Math.Max(blockWidth, blockHeight), 16, 112);
bool hasSelectedSample = sourcePoints.Length == 1;
if (sourcePoints.Length > 1)
{
for (int index = 0; index < sourcePoints.Length; index++)
{
int difference = Math.Abs(referencePoints[index].X - sourcePoints[index].X - motionVector.Column) +
Math.Abs(referencePoints[index].Y - sourcePoints[index].Y - motionVector.Row);
hasSelectedSample |= difference <= sampleThreshold;
}
}
int sourceCenterX = ((blockWidth >> 1) - 1) << 3;
int sourceCenterY = ((blockHeight >> 1) - 1) << 3;
int referenceCenterX = sourceCenterX + motionVector.Column;
int referenceCenterY = sourceCenterY + motionVector.Row;
int a00 = 0;
int a01 = 0;
int a11 = 0;
int bx0 = 0;
int bx1 = 0;
int by0 = 0;
int by1 = 0;
for (int index = 0; index < sourcePoints.Length; index++)
{
int motionVectorDifference = Math.Abs(referencePoints[index].X - sourcePoints[index].X - motionVector.Column) +
Math.Abs(referencePoints[index].Y - sourcePoints[index].Y - motionVector.Row);
// av1_selectSamples retains the original first sample when every candidate exceeds the threshold. Keeping
// that rule here is important because the selected Warped syntax still requires a deterministic model.
if (sourcePoints.Length > 1 && motionVectorDifference > sampleThreshold && (hasSelectedSample || index != 0))
{
continue;
}
int sourceX = sourcePoints[index].X - sourceCenterX;
int sourceY = sourcePoints[index].Y - sourceCenterY;
int referenceX = referencePoints[index].X - referenceCenterX;
int referenceY = referencePoints[index].Y - referenceCenterY;
if (Math.Abs(sourceX - referenceX) >= LocalProjectionMotionVectorLimit ||
Math.Abs(sourceY - referenceY) >= LocalProjectionMotionVectorLimit)
{
continue;
}
// These biased products are the normative reduced-precision P'P, P'q, and P'r matrices. Computing them
// directly preserves the reference decoder's integer least-squares rounding instead of introducing floating-point drift.
a00 += LeastSquaresSquare(sourceX);
a01 += LeastSquaresProduct1(sourceX, sourceY);
a11 += LeastSquaresSquare(sourceY);
bx0 += LeastSquaresProduct2(sourceX, referenceX);
bx1 += LeastSquaresProduct1(sourceY, referenceX);
by0 += LeastSquaresProduct1(sourceX, referenceY);
by1 += LeastSquaresProduct2(sourceY, referenceY);
}
long determinant = ((long)a00 * a11) - ((long)a01 * a01);
if (determinant == 0)
{
result.IsInvalid = true;
return result;
}
int inverseDeterminant = ResolveDivisor((ulong)Math.Abs(determinant), out int determinantShift) *
(determinant < 0 ? -1 : 1);
determinantShift -= ModelPrecisionBits;
if (determinantShift < 0)
{
inverseDeterminant <<= -determinantShift;
determinantShift = 0;
}
long projectionX0 = ((long)a11 * bx0) - ((long)a01 * bx1);
long projectionX1 = -((long)a01 * bx0) + ((long)a00 * bx1);
long projectionY0 = ((long)a11 * by0) - ((long)a01 * by1);
long projectionY1 = -((long)a01 * by0) + ((long)a00 * by1);
result.matrix[2] = ResolveProjectionCoefficient(
projectionX0,
inverseDeterminant,
determinantShift,
ModelScale - NonDiagonalAffineClamp + 1,
ModelScale + NonDiagonalAffineClamp - 1);
result.matrix[3] = ResolveProjectionCoefficient(
projectionX1,
inverseDeterminant,
determinantShift,
-NonDiagonalAffineClamp + 1,
NonDiagonalAffineClamp - 1);
result.matrix[4] = ResolveProjectionCoefficient(
projectionY0,
inverseDeterminant,
determinantShift,
-NonDiagonalAffineClamp + 1,
NonDiagonalAffineClamp - 1);
result.matrix[5] = ResolveProjectionCoefficient(
projectionY1,
inverseDeterminant,
determinantShift,
ModelScale - NonDiagonalAffineClamp + 1,
ModelScale + NonDiagonalAffineClamp - 1);
int absoluteCenterX = (modeInfoPosition.X << Av1Constants.ModeInfoSizeLog2) + (blockWidth >> 1) - 1;
int absoluteCenterY = (modeInfoPosition.Y << Av1Constants.ModeInfoSizeLog2) + (blockHeight >> 1) - 1;
int horizontalTranslation = (motionVector.Column << (ModelPrecisionBits - 3)) -
(absoluteCenterX * (result.matrix[2] - ModelScale)) -
(absoluteCenterY * result.matrix[3]);
int verticalTranslation = (motionVector.Row << (ModelPrecisionBits - 3)) -
(absoluteCenterX * result.matrix[4]) -
(absoluteCenterY * (result.matrix[5] - ModelScale));
result.matrix[0] = Math.Clamp(horizontalTranslation, -TranslationClamp, TranslationClamp - 1);
result.matrix[1] = Math.Clamp(verticalTranslation, -TranslationClamp, TranslationClamp - 1);
result.UpdateShearParameters();
return result;
}
/// <summary>
/// Derives the reduced shear parameters and records whether the complete affine model is valid.
/// </summary>
public void UpdateShearParameters()
{
Span<int> values = this.matrix;
this.Alpha = 0;
this.Beta = 0;
this.Gamma = 0;
this.Delta = 0;
if (values[2] <= 0)
{
this.IsInvalid = true;
return;
}
this.Alpha = (short)Math.Clamp(values[2] - ModelScale, short.MinValue, short.MaxValue);
this.Beta = (short)Math.Clamp(values[3], short.MinValue, short.MaxValue);
// AV1 derives gamma and delta by multiplying with a fixed-precision reciprocal of the horizontal scale.
// The reciprocal lookup is normative; integer division would produce different warped sample positions.
int reciprocal = ResolveDivisor((uint)values[2], out int reciprocalShift);
long scaledVerticalCoefficient = (long)values[4] * ModelScale * reciprocal;
this.Gamma = (short)Math.Clamp(RoundPowerOf2Signed(scaledVerticalCoefficient, reciprocalShift), short.MinValue, short.MaxValue);
long scaledCrossCoefficient = (long)values[3] * values[4] * reciprocal;
long verticalScaleDelta = values[5] - RoundPowerOf2Signed(scaledCrossCoefficient, reciprocalShift) - ModelScale;
this.Delta = (short)Math.Clamp(verticalScaleDelta, short.MinValue, short.MaxValue);
// Warped filtering addresses a coarser parameter grid than the stored affine matrix. Symmetric rounding is
// required here so negative shear values are quantized identically to their positive counterparts.
this.Alpha = ReduceShearParameter(this.Alpha);
this.Beta = ReduceShearParameter(this.Beta);
this.Gamma = ReduceShearParameter(this.Gamma);
this.Delta = ReduceShearParameter(this.Delta);
// These weighted L1 bounds are the AV1 validity test for the two shear axes. Equality is invalid because the
// warped-filter footprint would no longer remain inside the permitted affine sampling envelope.
this.IsInvalid =
((4 * Math.Abs((int)this.Alpha)) + (7 * Math.Abs((int)this.Beta)) >= ModelScale) ||
((4 * Math.Abs((int)this.Gamma)) + (4 * Math.Abs((int)this.Delta)) >= ModelScale);
}
/// <summary>
/// Quantizes one signed shear parameter to AV1's warped-filter precision.
/// </summary>
/// <param name="value">The full-precision shear parameter.</param>
/// <returns>The reduced shear parameter.</returns>
private static short ReduceShearParameter(short value)
=> (short)(RoundPowerOf2Signed(value, ShearParameterReductionBits) * (1 << ShearParameterReductionBits));
/// <summary>
/// Resolves a positive divisor into AV1's fixed-point reciprocal representation.
/// </summary>
/// <param name="divisor">The positive divisor.</param>
/// <param name="shift">Receives the reciprocal's binary scale.</param>
/// <returns>The fixed-point reciprocal multiplier.</returns>
private static int ResolveDivisor(uint divisor, out int shift)
{
// Normalize the divisor around its highest set bit, then quantize the remaining fraction to the normative
// eight-bit table index. Adding the table's fourteen fractional bits yields the scale used by the caller's
// rounded multiply instead of a platform-dependent integer division.
shift = BitOperations.Log2(divisor);
int remainder = (int)(divisor - (1U << shift));
int reciprocalIndex = shift > ReciprocalIndexBits
? RoundPowerOf2(remainder, shift - ReciprocalIndexBits)
: remainder << (ReciprocalIndexBits - shift);
shift += ReciprocalPrecisionBits;
return ReciprocalTable[reciprocalIndex];
}
/// <summary>
/// Resolves a positive 64-bit divisor into AV1's fixed-point reciprocal representation.
/// </summary>
/// <param name="divisor">The positive divisor.</param>
/// <param name="shift">Receives the reciprocal's binary scale.</param>
/// <returns>The fixed-point reciprocal multiplier.</returns>
private static int ResolveDivisor(ulong divisor, out int shift)
{
shift = BitOperations.Log2(divisor);
ulong remainder = divisor - (1UL << shift);
int reciprocalIndex = shift > ReciprocalIndexBits
? (int)((remainder + (1UL << (shift - ReciprocalIndexBits - 1))) >> (shift - ReciprocalIndexBits))
: (int)(remainder << (ReciprocalIndexBits - shift));
shift += ReciprocalPrecisionBits;
return ReciprocalTable[reciprocalIndex];
}
/// <summary>
/// Resolves one adjugate numerator into a clamped affine matrix coefficient.
/// </summary>
private static int ResolveProjectionCoefficient(long numerator, int inverseDeterminant, int shift, int minimum, int maximum)
{
long product = numerator * inverseDeterminant;
long value = shift > 0 ? RoundPowerOf2Signed(product, shift) : product << -shift;
return (int)Math.Clamp(value, minimum, maximum);
}
/// <summary>
/// Computes one reduced-precision diagonal element of the local projection matrix.
/// </summary>
private static int LeastSquaresSquare(int value)
=> ((value * value * 4) + (value * 32) + 128) >> 4;
/// <summary>
/// Computes one reduced-precision off-diagonal product of the local projection matrix.
/// </summary>
private static int LeastSquaresProduct1(int first, int second)
=> ((first * second * 4) + ((first + second) * 16) + 64) >> 4;
/// <summary>
/// Computes one reduced-precision source-to-reference product of the local projection matrix.
/// </summary>
private static int LeastSquaresProduct2(int first, int second)
=> ((first * second * 4) + ((first + second) * 16) + 128) >> 4;
/// <summary>
/// Divides a nonnegative integer by a power of two with nearest-integer rounding.
/// </summary>
/// <param name="value">The nonnegative value.</param>
/// <param name="bitCount">The base-two divisor exponent.</param>
/// <returns>The rounded quotient.</returns>
private static int RoundPowerOf2(int value, int bitCount)
=> (value + ((1 << bitCount) >> 1)) >> bitCount;
/// <summary>
/// Divides a signed integer by a power of two with symmetric nearest-integer rounding.
/// </summary>
/// <param name="value">The signed value.</param>
/// <param name="bitCount">The base-two divisor exponent.</param>
/// <returns>The rounded quotient.</returns>
private static long RoundPowerOf2Signed(long value, int bitCount)
=> value < 0
? -(((-value) + ((1L << bitCount) >> 1)) >> bitCount)
: (value + ((1L << bitCount) >> 1)) >> bitCount;
/// <summary>
/// Provides inline storage for the six parameters in an AV1 affine matrix.
/// </summary>
/// <typeparam name="T">The stored parameter type.</typeparam>
[InlineArray(6)]
private struct InlineArray6<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
}

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

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

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

@ -0,0 +1,415 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
/// <summary>
/// Derives and validates AV1 intra-block-copy displacement vectors.
/// </summary>
internal static class Av1IntraBlockCopy
{
/// <summary>
/// The number of surrounding mode-information rows and columns searched for reference vectors.
/// </summary>
private const int ReferenceSearchDistance = 3;
/// <summary>
/// The weight separating immediately adjacent candidates from the outer search area.
/// </summary>
private const int NearestCandidateWeight = 640;
/// <summary>
/// The number of 64-sample blocks that an intra-block-copy source must precede the active block.
/// </summary>
private const int Delay64 = 4;
/// <summary>
/// Finds the spatial reference used to differentially decode an intra-block-copy displacement vector.
/// </summary>
/// <param name="partitionInfo">The current block geometry and decoded neighbors.</param>
/// <param name="tileInfo">The active tile boundaries.</param>
/// <param name="superblockModeInfoSize">The superblock width in 4x4 mode-information units.</param>
/// <param name="candidates">Reusable storage for up to eight unique reference vectors.</param>
/// <param name="weights">Reusable storage for the corresponding spatial weights.</param>
/// <returns>The nearest nonzero spatial candidate, or the normative tile-relative fallback.</returns>
public static Av1MotionVector FindReference(
ref Av1PartitionInfo partitionInfo,
Av1TileInfo tileInfo,
int superblockModeInfoSize,
Span<Av1MotionVector> candidates,
Span<int> weights)
{
Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize;
int width = blockSize.Get4x4WideCount();
int height = blockSize.Get4x4HighCount();
int row = partitionInfo.RowIndex;
int column = partitionInfo.ColumnIndex;
int rowAdjustment = height < 2 && (row & 1) != 0 ? 1 : 0;
int columnAdjustment = width < 2 && (column & 1) != 0 ? 1 : 0;
int maximumRowOffset = 0;
int maximumColumnOffset = 0;
if (partitionInfo.AvailableAbove)
{
maximumRowOffset = height < 2 ? -4 + rowAdjustment : -(ReferenceSearchDistance << 1) + rowAdjustment;
maximumRowOffset = Math.Clamp(maximumRowOffset, tileInfo.ModeInfoRowStart - row, tileInfo.ModeInfoRowEnd - row - 1);
}
if (partitionInfo.AvailableLeft)
{
maximumColumnOffset = width < 2 ? -4 + columnAdjustment : -(ReferenceSearchDistance << 1) + columnAdjustment;
maximumColumnOffset = Math.Clamp(maximumColumnOffset, tileInfo.ModeInfoColumnStart - column, tileInfo.ModeInfoColumnEnd - column - 1);
}
int candidateCount = 0;
int processedRows = 0;
int processedColumns = 0;
if (Math.Abs(maximumRowOffset) >= 1)
{
ScanRow(ref partitionInfo, -1, maximumRowOffset, candidates, weights, ref candidateCount, ref processedRows);
}
if (Math.Abs(maximumColumnOffset) >= 1)
{
ScanColumn(ref partitionInfo, -1, maximumColumnOffset, candidates, weights, ref candidateCount, ref processedColumns);
}
if (partitionInfo.HasTopRight(superblockModeInfoSize))
{
AddBlock(ref partitionInfo, -1, width, tileInfo, candidates, weights, ref candidateCount);
}
int nearestCandidateCount = candidateCount;
for (int index = 0; index < nearestCandidateCount; index++)
{
weights[index] += NearestCandidateWeight;
}
// The top-left sample begins the outer search region. Sorting the adjacent and outer regions independently
// preserves the reference decoder's nearest/near ordering while still accumulating repeated vectors across both regions.
AddBlock(ref partitionInfo, -1, -1, tileInfo, candidates, weights, ref candidateCount);
for (int index = 2; index <= ReferenceSearchDistance; index++)
{
int rowOffset = -(index << 1) + 1 + rowAdjustment;
int columnOffset = -(index << 1) + 1 + columnAdjustment;
if (Math.Abs(rowOffset) <= Math.Abs(maximumRowOffset) && Math.Abs(rowOffset) > processedRows)
{
ScanRow(ref partitionInfo, rowOffset, maximumRowOffset, candidates, weights, ref candidateCount, ref processedRows);
}
if (Math.Abs(columnOffset) <= Math.Abs(maximumColumnOffset) && Math.Abs(columnOffset) > processedColumns)
{
ScanColumn(ref partitionInfo, columnOffset, maximumColumnOffset, candidates, weights, ref candidateCount, ref processedColumns);
}
}
SortByWeight(candidates, weights, 0, nearestCandidateCount);
SortByWeight(candidates, weights, nearestCandidateCount, candidateCount);
// The reference decoder clamps the ranked stack before selecting nearest and near. The displacement entropy syntax is
// differential, so using an unclamped spatial candidate changes every following component even though the
// final decoded displacement is validated separately against the stricter intra-block-copy source limits.
for (int index = 0; index < candidateCount; index++)
{
candidates[index] = candidates[index].ClampReference(
blockSize.GetWidth(),
blockSize.GetHeight(),
partitionInfo.ModeBlockToLeftEdge,
partitionInfo.ModeBlockToRightEdge,
partitionInfo.ModeBlockToTopEdge,
partitionInfo.ModeBlockToBottomEdge);
}
Av1MotionVector reference = candidateCount > 0 ? candidates[0] : default;
if (reference.IsZero && candidateCount > 1)
{
reference = candidates[1];
}
if (!reference.IsZero)
{
return reference;
}
const int modeInfoSampleSize = 1 << Av1Constants.ModeInfoSizeLog2;
const int eighthSampleScale = 8;
int fallbackRow = -modeInfoSampleSize * superblockModeInfoSize * eighthSampleScale;
int fallbackColumn = fallbackRow - (Delay64 * 64 * eighthSampleScale);
return (row - superblockModeInfoSize) < tileInfo.ModeInfoRowStart
? new Av1MotionVector(0, fallbackColumn)
: new Av1MotionVector(fallbackRow, 0);
}
/// <summary>
/// Determines whether a decoded displacement vector references an earlier reconstructable block inside the tile.
/// </summary>
/// <param name="vector">The decoded displacement vector in one-eighth-sample units.</param>
/// <param name="partitionInfo">The current block geometry.</param>
/// <param name="tileInfo">The active tile boundaries.</param>
/// <param name="sequenceHeader">The sequence-level superblock and chroma configuration.</param>
/// <returns><see langword="true"/> when the complete source block is a permitted reference; otherwise, <see langword="false"/>.</returns>
public static bool IsValid(Av1MotionVector vector, ref Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo, ObuSequenceHeader sequenceHeader)
{
const int eighthSampleScale = 8;
const int modeInfoSampleSize = 1 << Av1Constants.ModeInfoSizeLog2;
if ((vector.Row & (eighthSampleScale - 1)) != 0 || (vector.Column & (eighthSampleScale - 1)) != 0 ||
vector.Row <= -(1 << 14) || vector.Row >= (1 << 14) || vector.Column <= -(1 << 14) || vector.Column >= (1 << 14))
{
return false;
}
int row = partitionInfo.RowIndex;
int column = partitionInfo.ColumnIndex;
int blockWidth = partitionInfo.ModeInfo.BlockSize.GetWidth();
int blockHeight = partitionInfo.ModeInfo.BlockSize.GetHeight();
int sourceTop = (row * modeInfoSampleSize * eighthSampleScale) + vector.Row;
int sourceLeft = (column * modeInfoSampleSize * eighthSampleScale) + vector.Column;
int sourceBottom = (((row * modeInfoSampleSize) + blockHeight) * eighthSampleScale) + vector.Row;
int sourceRight = (((column * modeInfoSampleSize) + blockWidth) * eighthSampleScale) + vector.Column;
int tileTop = tileInfo.ModeInfoRowStart * modeInfoSampleSize * eighthSampleScale;
int tileLeft = tileInfo.ModeInfoColumnStart * modeInfoSampleSize * eighthSampleScale;
int tileBottom = tileInfo.ModeInfoRowEnd * modeInfoSampleSize * eighthSampleScale;
int tileRight = tileInfo.ModeInfoColumnEnd * modeInfoSampleSize * eighthSampleScale;
if (sourceTop < tileTop || sourceLeft < tileLeft || sourceBottom > tileBottom || sourceRight > tileRight)
{
return false;
}
ObuColorConfig colorConfig = sequenceHeader.ColorConfig;
if (partitionInfo.IsChroma && colorConfig.PlaneCount > 1)
{
// A sub-8x8 luma block can map to a chroma block whose rounded origin lies one additional luma unit
// inside the tile. These checks prevent that chroma reference from crossing the tile boundary.
if (blockWidth < 8 && colorConfig.SubSamplingX && sourceLeft < tileLeft + (modeInfoSampleSize * eighthSampleScale))
{
return false;
}
if (blockHeight < 8 && colorConfig.SubSamplingY && sourceTop < tileTop + (modeInfoSampleSize * eighthSampleScale))
{
return false;
}
}
int superblockModeInfoSize = sequenceHeader.SuperblockModeInfoSize;
int superblockSize = superblockModeInfoSize * modeInfoSampleSize;
int superblockModeInfoSizeLog2 = sequenceHeader.SuperblockSizeLog2 - Av1Constants.ModeInfoSizeLog2;
int activeSuperblockRow = row >> superblockModeInfoSizeLog2;
int active64Column = (column * modeInfoSampleSize) >> 6;
int sourceSuperblockRow = ((sourceBottom >> 3) - 1) / superblockSize;
int source64Column = ((sourceRight >> 3) - 1) >> 6;
int tile64ColumnCount = ((tileInfo.ModeInfoColumnEnd - tileInfo.ModeInfoColumnStart - 1) >> 4) + 1;
int active64 = (activeSuperblockRow * tile64ColumnCount) + active64Column;
int source64 = (sourceSuperblockRow * tile64ColumnCount) + source64Column;
if (source64 >= active64 - Delay64)
{
return false;
}
// The wavefront boundary reserves four completed 64-sample columns and advances farther right for every
// completed source row. A 128x128 superblock adds one column to account for its two 64-sample halves.
int gradient = 1 + Delay64 + (superblockSize > 64 ? 1 : 0);
int wavefrontOffset = gradient * (activeSuperblockRow - sourceSuperblockRow);
return sourceSuperblockRow <= activeSuperblockRow && source64Column < active64Column - Delay64 + wavefrontOffset;
}
/// <summary>
/// Scans a mode-information row using AV1's block-size-dependent steps and weights.
/// </summary>
private static void ScanRow(
ref Av1PartitionInfo partitionInfo,
int rowOffset,
int maximumRowOffset,
Span<Av1MotionVector> candidates,
Span<int> weights,
ref int candidateCount,
ref int processedRows)
{
int width = partitionInfo.ModeInfo.BlockSize.Get4x4WideCount();
int end = Math.Min(partitionInfo.GetMaxBlockWide(partitionInfo.ModeInfo.BlockSize, false), 16);
int columnOffset = 0;
if (Math.Abs(rowOffset) > 1)
{
columnOffset = 1;
if ((partitionInfo.ColumnIndex & 1) != 0 && width < 2)
{
columnOffset--;
}
}
// Blocks below 64 samples use the finer two-mode-info-unit scan step.
bool useFourUnitStep = width >= 16;
for (int index = 0; index < end;)
{
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(
new Point(partitionInfo.ColumnIndex + columnOffset + index, partitionInfo.RowIndex + rowOffset));
int candidateWidth = candidate.BlockSize.Get4x4WideCount();
int length = Math.Min(width, candidateWidth);
if (useFourUnitStep)
{
length = Math.Max(4, length);
}
else if (Math.Abs(rowOffset) > 1)
{
length = Math.Max(2, length);
}
int weight = 2;
if (width >= 2 && width <= candidateWidth)
{
int increment = Math.Min(-maximumRowOffset + rowOffset + 1, candidate.BlockSize.Get4x4HighCount());
weight = Math.Max(weight, increment);
processedRows = increment - rowOffset - 1;
}
AddCandidate(candidate, length * weight, candidates, weights, ref candidateCount);
index += length;
}
}
/// <summary>
/// Scans a mode-information column using AV1's block-size-dependent steps and weights.
/// </summary>
private static void ScanColumn(
ref Av1PartitionInfo partitionInfo,
int columnOffset,
int maximumColumnOffset,
Span<Av1MotionVector> candidates,
Span<int> weights,
ref int candidateCount,
ref int processedColumns)
{
int height = partitionInfo.ModeInfo.BlockSize.Get4x4HighCount();
int end = Math.Min(partitionInfo.GetMaxBlockHigh(partitionInfo.ModeInfo.BlockSize, false), 16);
int rowOffset = 0;
if (Math.Abs(columnOffset) > 1)
{
rowOffset = 1;
if ((partitionInfo.RowIndex & 1) != 0 && height < 2)
{
rowOffset--;
}
}
// Blocks below 64 samples use the finer two-mode-info-unit scan step.
bool useFourUnitStep = height >= 16;
for (int index = 0; index < end;)
{
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(
new Point(partitionInfo.ColumnIndex + columnOffset, partitionInfo.RowIndex + rowOffset + index));
int candidateHeight = candidate.BlockSize.Get4x4HighCount();
int length = Math.Min(height, candidateHeight);
if (useFourUnitStep)
{
length = Math.Max(4, length);
}
else if (Math.Abs(columnOffset) > 1)
{
length = Math.Max(2, length);
}
int weight = 2;
if (height >= 2 && height <= candidateHeight)
{
int increment = Math.Min(-maximumColumnOffset + columnOffset + 1, candidate.BlockSize.Get4x4WideCount());
weight = Math.Max(weight, increment);
processedColumns = increment - columnOffset - 1;
}
AddCandidate(candidate, length * weight, candidates, weights, ref candidateCount);
index += length;
}
}
/// <summary>
/// Adds the intra-block-copy vector at one tile-relative search position.
/// </summary>
private static void AddBlock(
ref Av1PartitionInfo partitionInfo,
int rowOffset,
int columnOffset,
Av1TileInfo tileInfo,
Span<Av1MotionVector> candidates,
Span<int> weights,
ref int candidateCount)
{
int row = partitionInfo.RowIndex + rowOffset;
int column = partitionInfo.ColumnIndex + columnOffset;
if (row < tileInfo.ModeInfoRowStart || row >= tileInfo.ModeInfoRowEnd ||
column < tileInfo.ModeInfoColumnStart || column >= tileInfo.ModeInfoColumnEnd)
{
return;
}
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column, row));
AddCandidate(candidate, 4, candidates, weights, ref candidateCount);
}
/// <summary>
/// Accumulates one unique intra-block-copy candidate and its spatial weight.
/// </summary>
private static void AddCandidate(
Av1BlockModeInfo candidate,
int weight,
Span<Av1MotionVector> candidates,
Span<int> weights,
ref int candidateCount)
{
if (!candidate.UseIntraBlockCopy)
{
return;
}
Av1MotionVector vector = candidate.DisplacementVector;
int index = 0;
for (; index < candidateCount; index++)
{
if (candidates[index] == vector)
{
weights[index] += weight;
return;
}
}
if (candidateCount < candidates.Length)
{
candidates[candidateCount] = vector;
weights[candidateCount] = weight;
candidateCount++;
}
}
/// <summary>
/// Sorts one candidate region by descending accumulated weight.
/// </summary>
private static void SortByWeight(Span<Av1MotionVector> candidates, Span<int> weights, int start, int end)
{
int length = end;
while (length > start)
{
int lastSwap = start;
for (int index = start + 1; index < length; index++)
{
if (weights[index - 1] < weights[index])
{
Av1MotionVector candidate = candidates[index - 1];
candidates[index - 1] = candidates[index];
candidates[index] = candidate;
int weight = weights[index - 1];
weights[index - 1] = weights[index];
weights[index] = weight;
lastSwap = index;
}
}
length = lastSwap;
}
}
}

297
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVariationCandidates.cs

@ -0,0 +1,297 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
/// <summary>
/// Derives the neighboring-block state and fixed-capacity projection samples used to select an AV1 motion mode.
/// </summary>
internal sealed class Av1MotionVariationCandidates
{
/// <summary>
/// The maximum number of neighboring motion samples retained for a local warped-motion projection.
/// </summary>
private const int ProjectionSampleCapacity = 8;
/// <summary>
/// The largest neighbor step used by overlapping motion compensation, measured in 4x4 mode-information units.
/// </summary>
private const int MaximumNeighborStep = 16;
/// <summary>
/// The number of fractional bits in an AV1 motion vector and warped-motion sample position.
/// </summary>
private const int MotionVectorSubpixelBits = 3;
/// <summary>
/// Stores sample positions relative to the current block origin in one-eighth-sample units.
/// </summary>
private InlineArray8<Point> sourcePoints;
/// <summary>
/// Stores the corresponding reference-frame positions in one-eighth-sample units.
/// </summary>
private InlineArray8<Point> referencePoints;
/// <summary>
/// Gets the number of valid entries in <see cref="SourcePoints"/> and <see cref="ReferencePoints"/>.
/// </summary>
public int Count { get; private set; }
/// <summary>
/// Gets a value indicating whether an inter-coded block overlaps the current block's above or left edge.
/// </summary>
public bool HasOverlappableNeighbor { get; private set; }
/// <summary>
/// Gets the retained current-frame sample positions in one-eighth-sample units relative to the current block.
/// </summary>
public ReadOnlySpan<Point> SourcePoints => this.sourcePoints[..this.Count];
/// <summary>
/// Gets the retained reference-frame sample positions in one-eighth-sample units relative to the current block.
/// </summary>
public ReadOnlySpan<Point> ReferencePoints => this.referencePoints[..this.Count];
/// <summary>
/// Derives the spatial state used to select Simple Translation, OBMC, or Warped motion for one inter block.
/// </summary>
/// <param name="partitionInfo">The current block geometry and frame-wide decoded mode map.</param>
/// <param name="tileInfo">The active tile boundaries.</param>
/// <param name="sequenceHeader">The sequence-level superblock geometry.</param>
/// <param name="frameHeader">The current frame dimensions.</param>
/// <param name="referenceFrame">The current block's primary canonical reference.</param>
public void Build(
ref Av1PartitionInfo partitionInfo,
Av1TileInfo tileInfo,
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader,
Av1ReferenceFrameType referenceFrame)
{
this.Count = 0;
this.CollectProjectionSamples(ref partitionInfo, tileInfo, sequenceHeader, frameHeader, referenceFrame);
this.HasOverlappableNeighbor = FindOverlappableNeighbor(ref partitionInfo, frameHeader);
}
/// <summary>
/// Collects the at most eight spatial samples permitted by AV1's local warped-motion model.
/// </summary>
/// <param name="partitionInfo">The current block geometry and frame-wide decoded mode map.</param>
/// <param name="tileInfo">The active tile boundaries.</param>
/// <param name="sequenceHeader">The sequence-level superblock geometry.</param>
/// <param name="frameHeader">The current frame dimensions.</param>
/// <param name="referenceFrame">The current block's primary canonical reference.</param>
private void CollectProjectionSamples(
ref Av1PartitionInfo partitionInfo,
Av1TileInfo tileInfo,
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader,
Av1ReferenceFrameType referenceFrame)
{
Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize;
int width = blockSize.Get4x4WideCount();
int height = blockSize.Get4x4HighCount();
int row = partitionInfo.RowIndex;
int column = partitionInfo.ColumnIndex;
bool includeTopLeft = true;
bool includeTopRight = true;
if (partitionInfo.AvailableAbove)
{
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column, row - 1));
int candidateWidth = candidate.BlockSize.Get4x4WideCount();
if (width <= candidateWidth)
{
// A wider above block can also cover the diagonal search positions. The signed alignment offset
// prevents those positions from contributing the same block a second time.
int columnOffset = -column % candidateWidth;
includeTopLeft = columnOffset >= 0;
includeTopRight = columnOffset + candidateWidth <= width;
this.AddProjectionSample(candidate, referenceFrame, 0, -1, columnOffset, 1);
}
else
{
int end = Math.Min(width, frameHeader.ModeInfoColumnCount - column);
for (int index = 0; index < end && this.Count < ProjectionSampleCapacity; index += candidateWidth)
{
candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column + index, row - 1));
candidateWidth = candidate.BlockSize.Get4x4WideCount();
this.AddProjectionSample(candidate, referenceFrame, 0, -1, index, 1);
}
}
}
if (partitionInfo.AvailableLeft && this.Count < ProjectionSampleCapacity)
{
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, row));
int candidateHeight = candidate.BlockSize.Get4x4HighCount();
if (height <= candidateHeight)
{
// The same alignment rule suppresses a duplicate top-left sample when one tall left block covers it.
int rowOffset = -row % candidateHeight;
includeTopLeft &= rowOffset >= 0;
this.AddProjectionSample(candidate, referenceFrame, rowOffset, 1, 0, -1);
}
else
{
int end = Math.Min(height, frameHeader.ModeInfoRowCount - row);
for (int index = 0; index < end && this.Count < ProjectionSampleCapacity; index += candidateHeight)
{
candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, row + index));
candidateHeight = candidate.BlockSize.Get4x4HighCount();
this.AddProjectionSample(candidate, referenceFrame, index, 1, 0, -1);
}
}
}
if (includeTopLeft && partitionInfo.AvailableAbove && partitionInfo.AvailableLeft && this.Count < ProjectionSampleCapacity)
{
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, row - 1));
this.AddProjectionSample(candidate, referenceFrame, 0, -1, 0, -1);
}
int topRightRow = row - 1;
int topRightColumn = column + width;
bool topRightInsideTile =
topRightRow >= tileInfo.ModeInfoRowStart &&
topRightRow < tileInfo.ModeInfoRowEnd &&
topRightColumn >= tileInfo.ModeInfoColumnStart &&
topRightColumn < tileInfo.ModeInfoColumnEnd;
if (includeTopRight &&
this.Count < ProjectionSampleCapacity &&
partitionInfo.HasTopRight(sequenceHeader.SuperblockModeInfoSize) &&
topRightInsideTile)
{
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(topRightColumn, topRightRow));
this.AddProjectionSample(candidate, referenceFrame, 0, -1, width, 1);
}
}
/// <summary>
/// Determines whether an inter-coded neighbor covers either complete prediction edge of the current block.
/// </summary>
/// <param name="partitionInfo">The current block geometry and frame-wide decoded mode map.</param>
/// <param name="frameHeader">The current frame dimensions.</param>
/// <returns><see langword="true"/> when an above or left inter block can contribute overlapping prediction.</returns>
private static bool FindOverlappableNeighbor(ref Av1PartitionInfo partitionInfo, ObuFrameHeader frameHeader)
{
Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize;
int width = blockSize.Get4x4WideCount();
int height = blockSize.Get4x4HighCount();
int row = partitionInfo.RowIndex;
int column = partitionInfo.ColumnIndex;
if (partitionInfo.AvailableAbove)
{
int endColumn = Math.Min(column + width, frameHeader.ModeInfoColumnCount);
for (int aboveColumn = column; aboveColumn < endColumn;)
{
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(aboveColumn, row - 1));
int step = Math.Min(candidate.BlockSize.Get4x4WideCount(), MaximumNeighborStep);
if (step == 1)
{
// AV1 treats a 4-sample-wide neighbor as one half of an 8-sample pair and reads the mode record
// attached to the pair's second cell before advancing across both cells.
aboveColumn &= ~1;
candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(aboveColumn + 1, row - 1));
step = 2;
}
if (IsOverlappable(candidate))
{
return true;
}
aboveColumn += step;
}
}
if (partitionInfo.AvailableLeft)
{
int endRow = Math.Min(row + height, frameHeader.ModeInfoRowCount);
for (int leftRow = row; leftRow < endRow;)
{
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, leftRow));
int step = Math.Min(candidate.BlockSize.Get4x4HighCount(), MaximumNeighborStep);
if (step == 1)
{
// The vertical scan applies the corresponding 4-sample-high pairing rule.
leftRow &= ~1;
candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, leftRow + 1));
step = 2;
}
if (IsOverlappable(candidate))
{
return true;
}
leftRow += step;
}
}
return false;
}
/// <summary>
/// Appends one neighboring single-reference sample when it uses the current block's primary reference.
/// </summary>
/// <param name="candidate">The neighboring decoded block.</param>
/// <param name="referenceFrame">The current block's primary canonical reference.</param>
/// <param name="rowOffset">The neighbor center row offset in 4x4 mode-information units.</param>
/// <param name="rowSign">The direction from the current block toward the neighbor on the vertical axis.</param>
/// <param name="columnOffset">The neighbor center column offset in 4x4 mode-information units.</param>
/// <param name="columnSign">The direction from the current block toward the neighbor on the horizontal axis.</param>
private void AddProjectionSample(
Av1BlockModeInfo candidate,
Av1ReferenceFrameType referenceFrame,
int rowOffset,
int rowSign,
int columnOffset,
int columnSign)
{
Span<Av1ReferenceFrameType> candidateReferences = candidate.ReferenceFrames;
if (candidateReferences[0] != referenceFrame || candidateReferences[1] != Av1ReferenceFrameType.None)
{
return;
}
const int modeInfoSampleSize = 1 << Av1Constants.ModeInfoSizeLog2;
int sourceX = (columnOffset * modeInfoSampleSize) + (columnSign * (candidate.BlockSize.GetWidth() >> 1)) - 1;
int sourceY = (rowOffset * modeInfoSampleSize) + (rowSign * (candidate.BlockSize.GetHeight() >> 1)) - 1;
Point sourcePoint = new(sourceX << MotionVectorSubpixelBits, sourceY << MotionVectorSubpixelBits);
Av1MotionVector motionVector = candidate.MotionVectors[0];
// Neighbor centers and motion vectors share Q3 precision. Adding them directly produces the corresponding
// reference position without rounding away the fractional displacement needed by the projection solver.
this.sourcePoints[this.Count] = sourcePoint;
this.referencePoints[this.Count] = new Point(sourcePoint.X + motionVector.Column, sourcePoint.Y + motionVector.Row);
this.Count++;
}
/// <summary>
/// Determines whether a decoded neighbor can participate in overlapping motion compensation.
/// </summary>
/// <param name="candidate">The neighboring decoded block.</param>
/// <returns><see langword="true"/> for inter prediction or intra-block copy; otherwise, <see langword="false"/>.</returns>
private static bool IsOverlappable(Av1BlockModeInfo candidate)
=> candidate.UseIntraBlockCopy || candidate.ReferenceFrames[0] > Av1ReferenceFrameType.Intra;
/// <summary>
/// Provides fixed storage for AV1's eight local warped-motion projection samples.
/// </summary>
/// <typeparam name="T">The source or reference point type stored in the inline buffer.</typeparam>
[InlineArray(ProjectionSampleCapacity)]
private struct InlineArray8<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
}

212
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVector.cs

@ -0,0 +1,212 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
/// <summary>
/// Represents an AV1 motion or displacement vector in one-eighth-sample units.
/// </summary>
internal readonly struct Av1MotionVector : IEquatable<Av1MotionVector>
{
/// <summary>
/// The greatest absolute temporal distance used by AV1 motion-vector projection.
/// </summary>
public const int MaximumTemporalDistance = 31;
/// <summary>
/// The reserved lower endpoint of the signed AV1 motion-vector domain.
/// </summary>
private const int LowerBound = -16384;
/// <summary>
/// The exclusive upper endpoint of the signed AV1 motion-vector domain.
/// </summary>
private const int UpperBound = 16384;
/// <summary>
/// The additional sixteen-sample border admitted while deriving spatial reference candidates, in one-eighth-sample units.
/// </summary>
private const int ReferenceBorder = 16 << 3;
/// <summary>
/// Initializes a new instance of the <see cref="Av1MotionVector"/> struct.
/// </summary>
/// <param name="row">The signed vertical displacement in one-eighth-sample units.</param>
/// <param name="column">The signed horizontal displacement in one-eighth-sample units.</param>
public Av1MotionVector(int row, int column)
{
this.Row = row;
this.Column = column;
}
/// <summary>
/// Gets the signed vertical displacement in one-eighth-sample units.
/// </summary>
public int Row { get; }
/// <summary>
/// Gets the signed horizontal displacement in one-eighth-sample units.
/// </summary>
public int Column { get; }
/// <summary>
/// Gets a value indicating whether both displacement components are zero.
/// </summary>
public bool IsZero => this.Row == 0 && this.Column == 0;
/// <summary>
/// Gets a value indicating whether both components lie strictly between the two reserved AV1 endpoints.
/// </summary>
public bool IsValid =>
this.Row > LowerBound &&
this.Row < UpperBound &&
this.Column > LowerBound &&
this.Column < UpperBound;
/// <summary>
/// Gets the reciprocal table used by AV1 temporal projection in fourteen-bit fixed-point precision.
/// </summary>
private static ReadOnlySpan<int> ProjectionDivisors =>
[0, 16384, 8192, 5461, 4096, 3276, 2730, 2340, 2048, 1820, 1638, 1489, 1365, 1260, 1170, 1092,
1024, 963, 910, 862, 819, 780, 744, 712, 682, 655, 630, 606, 585, 564, 546, 528];
/// <summary>
/// Adds a component delta to this vector.
/// </summary>
/// <param name="value">The reference vector.</param>
/// <param name="delta">The decoded component delta.</param>
/// <returns>The component-wise sum.</returns>
public static Av1MotionVector operator +(Av1MotionVector value, Av1MotionVector delta)
=> new(value.Row + delta.Row, value.Column + delta.Column);
/// <summary>
/// Determines whether two vectors have equal components.
/// </summary>
/// <param name="left">The first vector.</param>
/// <param name="right">The second vector.</param>
/// <returns><see langword="true"/> when both components are equal; otherwise, <see langword="false"/>.</returns>
public static bool operator ==(Av1MotionVector left, Av1MotionVector right) => left.Equals(right);
/// <summary>
/// Determines whether two vectors have different components.
/// </summary>
/// <param name="left">The first vector.</param>
/// <param name="right">The second vector.</param>
/// <returns><see langword="true"/> when either component differs; otherwise, <see langword="false"/>.</returns>
public static bool operator !=(Av1MotionVector left, Av1MotionVector right) => !left.Equals(right);
/// <summary>
/// Reduces this vector to the motion-vector precision selected by the current frame.
/// </summary>
/// <param name="allowHighPrecision">
/// A value indicating whether one-eighth-sample precision may be retained.
/// </param>
/// <param name="forceInteger">
/// A value indicating whether both components must be rounded to integer-sample precision.
/// </param>
/// <returns>The precision-reduced vector.</returns>
public Av1MotionVector LowerPrecision(bool allowHighPrecision, bool forceInteger)
{
if (forceInteger)
{
return new(RoundToIntegerPrecision(this.Row), RoundToIntegerPrecision(this.Column));
}
if (allowHighPrecision)
{
return this;
}
// Low precision removes the one-eighth-sample bit. Odd components move toward zero rather than rounding to
// the nearest even value, which is the normative lower_mv_precision behavior used by spatial and temporal MVs.
int row = (this.Row & 1) != 0 ? this.Row + (this.Row > 0 ? -1 : 1) : this.Row;
int column = (this.Column & 1) != 0 ? this.Column + (this.Column > 0 ? -1 : 1) : this.Column;
return new(row, column);
}
/// <summary>
/// Clamps this vector to the spatial reference-candidate limits for a coding block.
/// </summary>
/// <param name="blockWidth">The coding-block width in luma samples.</param>
/// <param name="blockHeight">The coding-block height in luma samples.</param>
/// <param name="blockToLeftEdge">The signed distance to the left frame edge in one-eighth-sample units.</param>
/// <param name="blockToRightEdge">The signed distance to the right frame edge in one-eighth-sample units.</param>
/// <param name="blockToTopEdge">The signed distance to the top frame edge in one-eighth-sample units.</param>
/// <param name="blockToBottomEdge">The signed distance to the bottom frame edge in one-eighth-sample units.</param>
/// <returns>The vector clamped to the permitted spatial reference-candidate range.</returns>
public Av1MotionVector ClampReference(
int blockWidth,
int blockHeight,
int blockToLeftEdge,
int blockToRightEdge,
int blockToTopEdge,
int blockToBottomEdge)
{
int blockWidthSubpixel = blockWidth << 3;
int blockHeightSubpixel = blockHeight << 3;
// Candidate derivation permits the complete block extent plus sixteen further luma samples beyond each
// visible frame edge. These are stack limits, not the tighter UMV limits applied later while sampling pixels.
int minimumColumn = blockToLeftEdge - blockWidthSubpixel - ReferenceBorder;
int maximumColumn = blockToRightEdge + blockWidthSubpixel + ReferenceBorder;
int minimumRow = blockToTopEdge - blockHeightSubpixel - ReferenceBorder;
int maximumRow = blockToBottomEdge + blockHeightSubpixel + ReferenceBorder;
return new(
Av1Math.Clip3(minimumRow, maximumRow, this.Row),
Av1Math.Clip3(minimumColumn, maximumColumn, this.Column));
}
/// <summary>
/// Projects this vector across a ratio of temporal frame distances.
/// </summary>
/// <param name="numerator">The signed source-to-target frame distance.</param>
/// <param name="denominator">The positive source-to-reference frame distance.</param>
/// <returns>The projected vector clamped inside the AV1 motion-vector domain.</returns>
public Av1MotionVector ProjectTemporal(int numerator, int denominator)
{
denominator = Math.Min(denominator, MaximumTemporalDistance);
numerator = Av1Math.Clip3(-MaximumTemporalDistance, MaximumTemporalDistance, numerator);
// The reciprocal table represents 1 / denominator in Q14. Signed power-of-two rounding preserves symmetry
// for negative components, and AV1 excludes the two reserved endpoints from projected motion vectors.
// Motion-field retention limits each source component to 4095, keeping the complete Q14 product inside Int32.
int row = Av1Math.RoundPowerOf2Signed(this.Row * numerator * ProjectionDivisors[denominator], 14);
int column = Av1Math.RoundPowerOf2Signed(this.Column * numerator * ProjectionDivisors[denominator], 14);
row = Av1Math.Clip3(LowerBound + 1, UpperBound - 1, row);
column = Av1Math.Clip3(LowerBound + 1, UpperBound - 1, column);
return new(row, column);
}
/// <summary>
/// Determines whether this vector has the same components as another vector.
/// </summary>
/// <param name="other">The vector to compare.</param>
/// <returns><see langword="true"/> when both components are equal; otherwise, <see langword="false"/>.</returns>
public bool Equals(Av1MotionVector other) => this.Row == other.Row && this.Column == other.Column;
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is Av1MotionVector other && this.Equals(other);
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.Row, this.Column);
/// <summary>
/// Rounds one component to the nearest integer-sample displacement.
/// </summary>
/// <param name="value">The component in one-eighth-sample units.</param>
/// <returns>The integer-precision component in one-eighth-sample units.</returns>
private static int RoundToIntegerPrecision(int value)
{
int remainder = value % 8;
value -= remainder;
// Exactly half an integer sample has magnitude four. AV1 leaves that truncated base unchanged, so both
// positive and negative half ties move toward zero; only larger remainders advance to the adjacent sample.
if (Math.Abs(remainder) > 4)
{
value += remainder > 0 ? 8 : -8;
}
return value;
}
}

25
src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVectorPrecision.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
/// <summary>
/// Identifies the fractional precision used to decode an AV1 motion-vector delta.
/// </summary>
internal enum Av1MotionVectorPrecision : sbyte
{
/// <summary>
/// Restricts components to whole-sample increments.
/// </summary>
Integer = -1,
/// <summary>
/// Allows components in quarter-sample increments.
/// </summary>
QuarterSample,
/// <summary>
/// Allows components in eighth-sample increments.
/// </summary>
EighthSample
}

1394
src/ImageSharp/Formats/Heif/Av1/Motion/Av1ReferenceMotionVectors.cs

File diff suppressed because it is too large

30
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuChromoSamplePosition.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Identifies the position of chroma samples relative to luma samples.
/// </summary>
internal enum ObuChromoSamplePosition : byte
{
/// <summary>
/// Unknown.
/// </summary>
Unknown = 0,
/// <summary>
/// The chroma sample is horizontally co-located with the top-left luma sample and lies between two luma rows.
/// </summary>
Vertical = 1,
/// <summary>
/// The chroma sample is co-located with the top-left luma sample.
/// </summary>
Colocated = 2,
/// <summary>
/// Reserved and invalid for AV1 content.
/// </summary>
Reserved = 3,
}

116
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorConfig.cs

@ -0,0 +1,116 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the color configuration signaled by an AV1 sequence header.
/// </summary>
internal sealed class ObuColorConfig
{
/// <summary>
/// Stores whether the sequence uses a single monochrome plane.
/// </summary>
private bool isMonochrome;
/// <summary>
/// Gets or sets a value indicating whether color-description syntax is present.
/// </summary>
public bool IsColorDescriptionPresent { get; set; }
/// <summary>
/// Gets the number of color channels in this image. Can have the value 1 or 3.
/// </summary>
public int PlaneCount { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether the image has a single greyscale plane, will have
/// <see cref="Av1Constants.MaxPlanes"/> color planes otherwise.
/// </summary>
public bool IsMonochrome
{
get => this.isMonochrome;
set
{
// Plane count is derived from the monochrome flag throughout the decoder, so update
// both values atomically rather than allowing the two pieces of state to diverge.
this.PlaneCount = value ? 1 : Av1Constants.MaxPlanes;
this.isMonochrome = value;
}
}
/// <summary>
/// Gets or sets the color-primary chromaticities.
/// </summary>
public ObuColorPrimaries ColorPrimaries { get; set; }
/// <summary>
/// Gets or sets the transfer characteristics.
/// </summary>
public ObuTransferCharacteristics TransferCharacteristics { get; set; }
/// <summary>
/// Gets or sets the matrix coefficients used to derive luma and chroma components.
/// </summary>
public ObuMatrixCoefficients MatrixCoefficients { get; set; }
/// <summary>
/// Gets or sets a value indicating whether samples use the full numeric range.
/// </summary>
public bool ColorRange { get; set; }
/// <summary>
/// Gets or sets a value indicating whether chroma is subsampled horizontally.
/// </summary>
public bool SubSamplingX { get; set; }
/// <summary>
/// Gets or sets a value indicating whether chroma is subsampled vertically.
/// </summary>
public bool SubSamplingY { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the U and V planes use separate quantizer deltas.
/// </summary>
public bool HasSeparateUvDelta { get; set; }
/// <summary>
/// Gets or sets the chroma sample position for vertically subsampled images.
/// </summary>
public ObuChromoSamplePosition ChromaSamplePosition { get; set; }
/// <summary>
/// Gets or sets the encoded sample bit depth.
/// </summary>
public Av1BitDepth BitDepth { get; set; }
/// <summary>
/// Gets the color format represented by the monochrome and chroma-subsampling flags.
/// </summary>
/// <returns>The corresponding AV1 color format.</returns>
public Av1ColorFormat GetColorFormat()
{
if (this.IsMonochrome)
{
// AV1 sets both subsampling flags for monochrome sequences even though no chroma planes exist. The
// mono_chrome syntax therefore owns the plane layout and must take precedence over those derived flags.
return Av1ColorFormat.Yuv400;
}
Av1ColorFormat format = Av1ColorFormat.Yuv400;
if (this.SubSamplingX && this.SubSamplingY)
{
format = Av1ColorFormat.Yuv420;
}
else if (this.SubSamplingX & !this.SubSamplingY)
{
format = Av1ColorFormat.Yuv422;
}
else if (!this.SubSamplingX && !this.SubSamplingY)
{
format = Av1ColorFormat.Yuv444;
}
return format;
}
}

75
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorPrimaries.cs

@ -0,0 +1,75 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Identifies the CICP color-primary chromaticities signaled by an AV1 sequence.
/// </summary>
internal enum ObuColorPrimaries
{
/// <summary>
/// The reserved zero value.
/// </summary>
Reserved = 0,
/// <summary>
/// ITU-R BT.709 primaries.
/// </summary>
Bt709 = 1,
/// <summary>
/// Unspecified primaries.
/// </summary>
Unspecified = 2,
/// <summary>
/// ITU-R BT.470 System M primaries.
/// </summary>
Bt470M = 4,
/// <summary>
/// ITU-R BT.470 System B and G primaries.
/// </summary>
Bt470BG = 5,
/// <summary>
/// ITU-R BT.601 primaries.
/// </summary>
Bt601 = 6,
/// <summary>
/// SMPTE 240M primaries.
/// </summary>
Smpte240 = 7,
/// <summary>
/// Generic film primaries.
/// </summary>
GenericFilm = 8,
/// <summary>
/// ITU-R BT.2020 and BT.2100 primaries.
/// </summary>
Bt2020 = 9,
/// <summary>
/// SMPTE ST 428 CIE XYZ primaries.
/// </summary>
Xyz = 10,
/// <summary>
/// SMPTE RP 431-2 primaries.
/// </summary>
Smpte431 = 11,
/// <summary>
/// SMPTE EG 432-1 primaries.
/// </summary>
Smpte432 = 12,
/// <summary>
/// EBU Tech. 3213-E primaries.
/// </summary>
Ebu3213 = 22,
}

40
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuConstraintDirectionalEnhancementFilterParameters.cs

@ -0,0 +1,40 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the constrained directional enhancement filter parameters for an AV1 frame.
/// </summary>
internal sealed class ObuConstraintDirectionalEnhancementFilterParameters
{
/// <summary>
/// Stores the fixed sixteen luma filter strengths without a per-frame array allocation.
/// </summary>
private InlineArray16<int> yStrength;
/// <summary>
/// Stores the fixed sixteen chroma filter strengths without a per-frame array allocation.
/// </summary>
private InlineArray16<int> uvStrength;
/// <summary>
/// Gets or sets the number of bits used to select a filter-strength entry.
/// </summary>
public int BitCount { get; set; }
/// <summary>
/// Gets or sets the filter damping value.
/// </summary>
public int Damping { get; set; } = 3;
/// <summary>
/// Gets the primary and secondary luma strengths for each filter entry.
/// </summary>
public Span<int> YStrength => this.yStrength;
/// <summary>
/// Gets the primary and secondary chroma strengths for each filter entry.
/// </summary>
public Span<int> UvStrength => this.uvStrength;
}

32
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDecoderModelInfo.cs

@ -0,0 +1,32 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Contains the field widths and decoding-clock units used by the AV1 decoder model.
/// </summary>
internal sealed class ObuDecoderModelInfo
{
/// <summary>
/// Gets or sets BufferDelayLength. Specifies the length of the decoder_buffer_delay and the encoder_buffer_delay
/// syntax elements, in bits.
/// </summary>
public uint BufferDelayLength { get; set; }
/// <summary>
/// Gets or sets NumUnitsInDecodingTick. This is the number of time units of a decoding clock operating at the frequency time_scale Hz
/// that corresponds to one increment of a clock tick counter.
/// </summary>
public uint NumUnitsInDecodingTick { get; set; }
/// <summary>
/// Gets or sets BufferRemovalTimeLength. Specifies the length of the buffer_removal_time syntax element, in bits.
/// </summary>
public uint BufferRemovalTimeLength { get; set; }
/// <summary>
/// Gets or sets the FramePresentationTimeLength. Specifies the length of the frame_presentation_time syntax element, in bits.
/// </summary>
public uint FramePresentationTimeLength { get; set; }
}

25
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDeltaParameters.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the delta-quantizer or delta-loop-filter signaling parameters for an AV1 frame.
/// </summary>
internal sealed class ObuDeltaParameters
{
/// <summary>
/// Gets or sets a value indicating whether per-block delta values are present.
/// </summary>
public bool IsPresent { get; set; }
/// <summary>
/// Gets or sets the delta-value multiplier, which is one, two, four, or eight.
/// </summary>
public int Resolution { get; set; }
/// <summary>
/// Gets or sets a value indicating whether separate loop-filter deltas are signaled for multiple filter targets.
/// </summary>
public bool IsMulti { get; set; }
}

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

@ -0,0 +1,292 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the parameters used to synthesize AV1 film grain for a decoded frame.
/// </summary>
internal sealed class ObuFilmGrainParameters
{
/// <summary>
/// Stores the luma scaling-point coordinates without a per-frame array allocation.
/// </summary>
private InlineArray14<byte> pointYValue;
/// <summary>
/// Stores the luma scaling-point values without a per-frame array allocation.
/// </summary>
private InlineArray14<byte> pointYScaling;
/// <summary>
/// Stores the blue-difference scaling-point coordinates without a per-frame array allocation.
/// </summary>
private InlineArray10<byte> pointCbValue;
/// <summary>
/// Stores the blue-difference scaling-point values without a per-frame array allocation.
/// </summary>
private InlineArray10<byte> pointCbScaling;
/// <summary>
/// Stores the red-difference scaling-point coordinates without a per-frame array allocation.
/// </summary>
private InlineArray10<byte> pointCrValue;
/// <summary>
/// Stores the red-difference scaling-point values without a per-frame array allocation.
/// </summary>
private InlineArray10<byte> pointCrScaling;
/// <summary>
/// Stores the luma autoregressive coefficients without a per-frame array allocation.
/// </summary>
private InlineArray24<byte> arCoeffsYPlus128;
/// <summary>
/// Stores the blue-difference autoregressive coefficients without a per-frame array allocation.
/// </summary>
private InlineArray25<byte> arCoeffsCbPlus128;
/// <summary>
/// Stores the red-difference autoregressive coefficients without a per-frame array allocation.
/// </summary>
private InlineArray25<byte> arCoeffsCrPlus128;
/// <summary>
/// Gets or sets a value indicating whether film grain is applied to the displayed frame.
/// </summary>
public bool ApplyGrain { get; set; }
/// <summary>
/// Gets or sets the 16-bit seed that initializes pseudo-random film-grain synthesis for this frame.
/// </summary>
public uint GrainSeed { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this frame signals a complete parameter set instead of inheriting one.
/// </summary>
public bool UpdateGrain { get; set; }
/// <summary>
/// Gets or sets the physical reference-map index from which this frame inherited its film-grain parameters.
/// The index must match one of the frame's seven selected inter-reference slots.
/// </summary>
public uint FilmGrainParamsRefIdx { get; set; }
/// <summary>
/// Gets or sets the number of active luma scaling points in the inclusive range zero through fourteen.
/// </summary>
public uint NumYPoints { get; set; }
/// <summary>
/// Gets the fourteen-entry storage for the luma scaling-point coordinates.
/// </summary>
/// <remarks>
/// Only the first <see cref="NumYPoints"/> entries are active. Coordinates use the eight-bit scale and must be
/// strictly increasing; 10-bit and 12-bit sample values are divided by four and sixteen respectively.
/// </remarks>
public Span<byte> PointYValue => this.pointYValue;
/// <summary>
/// Gets the fourteen-entry storage for the luma scaling-point output values.
/// </summary>
/// <remarks>Only the first <see cref="NumYPoints"/> entries are active.</remarks>
public Span<byte> PointYScaling => this.pointYScaling;
/// <summary>
/// Gets or sets a value indicating whether both chroma scaling functions are derived from luma samples.
/// </summary>
public bool ChromaScalingFromLuma { get; set; }
/// <summary>
/// Gets or sets the number of active blue-difference scaling points in the inclusive range zero through ten.
/// </summary>
public uint NumCbPoints { get; set; }
/// <summary>
/// Gets or sets the number of active red-difference scaling points in the inclusive range zero through ten.
/// </summary>
public uint NumCrPoints { get; set; }
/// <summary>
/// Gets the ten-entry storage for the blue-difference scaling-point coordinates.
/// </summary>
/// <remarks>Only the first <see cref="NumCbPoints"/> entries are active, and active coordinates must be strictly increasing.</remarks>
public Span<byte> PointCbValue => this.pointCbValue;
/// <summary>
/// Gets the ten-entry storage for the blue-difference scaling-point output values.
/// </summary>
/// <remarks>Only the first <see cref="NumCbPoints"/> entries are active.</remarks>
public Span<byte> PointCbScaling => this.pointCbScaling;
/// <summary>
/// Gets the ten-entry storage for the red-difference scaling-point coordinates.
/// </summary>
/// <remarks>Only the first <see cref="NumCrPoints"/> entries are active, and active coordinates must be strictly increasing.</remarks>
public Span<byte> PointCrValue => this.pointCrValue;
/// <summary>
/// Gets the ten-entry storage for the red-difference scaling-point output values.
/// </summary>
/// <remarks>Only the first <see cref="NumCrPoints"/> entries are active.</remarks>
public Span<byte> PointCrScaling => this.pointCrScaling;
/// <summary>
/// Gets or sets the scaling-function shift minus eight. Values from zero through three select an effective shift
/// from eight through eleven for every luma and chroma scaling value.
/// </summary>
public uint GrainScalingMinus8 { get; set; }
/// <summary>
/// Gets or sets the autoregressive neighborhood lag in the inclusive range zero through three.
/// </summary>
public uint ArCoeffLag { get; set; }
/// <summary>
/// Gets the twenty-four-entry storage for biased luma autoregressive coefficients.
/// </summary>
/// <remarks>The active entry count is <c>2 * ArCoeffLag * (ArCoeffLag + 1)</c>.</remarks>
public Span<byte> ArCoeffsYPlus128 => this.arCoeffsYPlus128;
/// <summary>
/// Gets the twenty-five-entry storage for biased blue-difference autoregressive coefficients.
/// </summary>
/// <remarks>The active entry count includes one additional luma coefficient when luma scaling points are present.</remarks>
public Span<byte> ArCoeffsCbPlus128 => this.arCoeffsCbPlus128;
/// <summary>
/// Gets the twenty-five-entry storage for biased red-difference autoregressive coefficients.
/// </summary>
/// <remarks>The active entry count includes one additional luma coefficient when luma scaling points are present.</remarks>
public Span<byte> ArCoeffsCrPlus128 => this.arCoeffsCrPlus128;
/// <summary>
/// Gets or sets the autoregressive coefficient shift minus six in the inclusive range zero through three.
/// </summary>
public uint ArCoeffShiftMinus6 { get; set; }
/// <summary>
/// Gets or sets the right shift applied to generated Gaussian grain samples in the inclusive range zero through three.
/// </summary>
public uint GrainScaleShift { get; set; }
/// <summary>
/// Gets or sets the 8-bit blue-difference sample multiplier used to derive the chroma scaling index.
/// </summary>
public uint CbMult { get; set; }
/// <summary>
/// Gets or sets the 8-bit average-luma multiplier used to derive the blue-difference scaling index.
/// </summary>
public uint CbLumaMult { get; set; }
/// <summary>
/// Gets or sets the 9-bit offset used to derive the blue-difference scaling index.
/// </summary>
public uint CbOffset { get; set; }
/// <summary>
/// Gets or sets the 8-bit red-difference sample multiplier used to derive the chroma scaling index.
/// </summary>
public uint CrMult { get; set; }
/// <summary>
/// Gets or sets the 8-bit average-luma multiplier used to derive the red-difference scaling index.
/// </summary>
public uint CrLumaMult { get; set; }
/// <summary>
/// Gets or sets the 9-bit offset used to derive the red-difference scaling index.
/// </summary>
public uint CrOffset { get; set; }
/// <summary>
/// Gets or sets a value indicating whether neighboring film-grain blocks are blended across their boundaries.
/// </summary>
public bool OverlapFlag { get; set; }
/// <summary>
/// Gets or sets a value indicating whether grained samples are clipped to the restricted range instead of the full range.
/// </summary>
public bool ClipToRestrictedRange { get; set; }
/// <summary>
/// Replaces the complete film-grain parameter set with values retained by a reference frame.
/// </summary>
/// <param name="source">The retained reference-frame parameters.</param>
public void CopyFrom(ObuFilmGrainParameters source)
{
this.ApplyGrain = source.ApplyGrain;
this.GrainSeed = source.GrainSeed;
this.UpdateGrain = source.UpdateGrain;
this.FilmGrainParamsRefIdx = source.FilmGrainParamsRefIdx;
this.NumYPoints = source.NumYPoints;
this.pointYValue = source.pointYValue;
this.pointYScaling = source.pointYScaling;
this.ChromaScalingFromLuma = source.ChromaScalingFromLuma;
this.NumCbPoints = source.NumCbPoints;
this.NumCrPoints = source.NumCrPoints;
this.pointCbValue = source.pointCbValue;
this.pointCbScaling = source.pointCbScaling;
this.pointCrValue = source.pointCrValue;
this.pointCrScaling = source.pointCrScaling;
this.GrainScalingMinus8 = source.GrainScalingMinus8;
this.ArCoeffLag = source.ArCoeffLag;
this.arCoeffsYPlus128 = source.arCoeffsYPlus128;
this.arCoeffsCbPlus128 = source.arCoeffsCbPlus128;
this.arCoeffsCrPlus128 = source.arCoeffsCrPlus128;
this.ArCoeffShiftMinus6 = source.ArCoeffShiftMinus6;
this.GrainScaleShift = source.GrainScaleShift;
this.CbMult = source.CbMult;
this.CbLumaMult = source.CbLumaMult;
this.CbOffset = source.CbOffset;
this.CrMult = source.CrMult;
this.CrLumaMult = source.CrLumaMult;
this.CrOffset = source.CrOffset;
this.OverlapFlag = source.OverlapFlag;
this.ClipToRestrictedRange = source.ClipToRestrictedRange;
}
/// <summary>
/// Provides inline storage for the maximum luma autoregressive coefficient count.
/// </summary>
/// <typeparam name="T">The stored value type.</typeparam>
[InlineArray(24)]
private struct InlineArray24<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
/// <summary>
/// Provides inline storage for the ten scaling points permitted on either chroma plane.
/// </summary>
/// <typeparam name="T">The stored value type.</typeparam>
[InlineArray(10)]
private struct InlineArray10<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
/// <summary>
/// Provides inline storage for the maximum autoregressive coefficient count of either chroma plane.
/// </summary>
/// <typeparam name="T">The stored value type.</typeparam>
[InlineArray(25)]
private struct InlineArray25<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
}

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

@ -0,0 +1,357 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Stores the decoded syntax and derived state for one AV1 frame header.
/// </summary>
internal sealed class ObuFrameHeader
{
/// <summary>
/// Stores the validity state of the eight reference-frame slots without a per-header array allocation.
/// </summary>
private InlineArray8<bool> referenceValid;
/// <summary>
/// Stores the multi-bit order hint associated with each of the eight reference-frame slots.
/// </summary>
private InlineArray8<uint> referenceOrderHint;
/// <summary>
/// Stores the frame identifier associated with each of the eight reference-map slots.
/// </summary>
private InlineArray8<uint> referenceFrameId;
/// <summary>
/// Stores the reference-map slot selected for each of the seven inter reference types.
/// </summary>
private InlineArray8<uint> referenceFrameIndex;
/// <summary>
/// Stores the global-motion model associated with each of the seven inter reference types.
/// </summary>
private InlineArray7<Av1GlobalMotionParameters> globalMotionParameters;
/// <summary>
/// Stores the lossless-coding flag for each of the eight segments without a per-header array allocation.
/// </summary>
private InlineArray8<bool> losslessArray;
/// <summary>
/// Gets or sets the temporal-layer identifier carried by the primary frame-header OBU.
/// </summary>
public int TemporalId { get; set; }
/// <summary>
/// Gets or sets the spatial-layer identifier carried by the primary frame-header OBU.
/// </summary>
public int SpatialId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether motion vectors use integer-sample precision.
/// </summary>
public bool ForceIntegerMotionVector { get; set; }
/// <summary>
/// Gets or sets a value indicating whether intra-block copy is permitted.
/// </summary>
public bool AllowIntraBlockCopy { get; set; }
/// <summary>
/// Gets or sets a value indicating whether reference-frame motion vectors may be used.
/// </summary>
public bool UseReferenceFrameMotionVectors { get; set; }
/// <summary>
/// Gets or sets a value indicating whether motion vectors may use high precision.
/// </summary>
public bool AllowHighPrecisionMotionVector { get; set; }
/// <summary>
/// Gets or sets the frame-level interpolation filter used for inter prediction.
/// </summary>
public Av1InterpolationFilter InterpolationFilter { get; set; }
/// <summary>
/// Gets or sets a value indicating whether inter blocks may select a non-translational motion mode.
/// </summary>
public bool IsMotionModeSwitchable { get; set; }
/// <summary>
/// Gets or sets the decoded tile layout.
/// </summary>
public ObuTileGroupHeader TilesInfo { get; set; } = new ObuTileGroupHeader();
/// <summary>
/// Gets or sets a value indicating whether every segment uses lossless coding.
/// </summary>
public bool CodedLossless { get; set; }
/// <summary>
/// Gets the mutable lossless-coding flags for each segment.
/// </summary>
public Span<bool> LosslessArray => this.losslessArray;
/// <summary>
/// Gets or sets the frame quantization parameters.
/// </summary>
public ObuQuantizationParameters QuantizationParameters { get; set; } = new ObuQuantizationParameters();
/// <summary>
/// Gets or sets the frame segmentation parameters.
/// </summary>
public ObuSegmentationParameters SegmentationParameters { get; set; } = new ObuSegmentationParameters();
/// <summary>
/// Gets or sets a value indicating whether coding is lossless and no super-resolution scaling is applied.
/// </summary>
public bool AllLossless { get; set; }
/// <summary>
/// Gets or sets a value indicating whether warped motion is permitted.
/// </summary>
public bool AllowWarpedMotion { get; set; }
/// <summary>
/// Gets or sets the permitted reference prediction mode.
/// </summary>
public ObuReferenceMode ReferenceMode { get; set; }
/// <summary>
/// Gets or sets the film-grain synthesis parameters.
/// </summary>
public ObuFilmGrainParameters FilmGrainParameters { get; set; } = new ObuFilmGrainParameters();
/// <summary>
/// Gets or sets a value indicating whether the reduced transform set is used.
/// </summary>
public bool UseReducedTransformSet { get; set; }
/// <summary>
/// Gets or sets the loop-filter parameters.
/// </summary>
public ObuLoopFilterParameters LoopFilterParameters { get; set; } = new ObuLoopFilterParameters();
/// <summary>
/// Gets or sets the loop-restoration parameters.
/// </summary>
public ObuLoopRestorationParameters LoopRestorationParameters { get; set; } = new ObuLoopRestorationParameters();
/// <summary>
/// Gets or sets the constrained directional enhancement filter parameters.
/// </summary>
public ObuConstraintDirectionalEnhancementFilterParameters CdefParameters { get; set; } = new ObuConstraintDirectionalEnhancementFilterParameters();
/// <summary>
/// Gets or sets the number of mode-information columns in one stored row.
/// </summary>
public int ModeInfoStride { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the frame-end probability update is disabled.
/// </summary>
public bool DisableFrameEndUpdateCdf { get; set; }
/// <summary>
/// Gets or sets the skip-mode parameters.
/// </summary>
public ObuSkipModeParameters SkipModeParameters { get; set; } = new ObuSkipModeParameters();
/// <summary>
/// Gets or sets the transform-size selection mode.
/// </summary>
public Av1TransformMode TransformMode { get; set; }
/// <summary>
/// Gets or sets the loop-filter delta parameters.
/// </summary>
public ObuDeltaParameters DeltaLoopFilterParameters { get; set; } = new ObuDeltaParameters();
/// <summary>
/// Gets or sets the quantizer delta parameters.
/// </summary>
public ObuDeltaParameters DeltaQParameters { get; set; } = new ObuDeltaParameters();
/// <summary>
/// Gets a value indicating whether the frame uses intra prediction only.
/// </summary>
public bool IsIntra => this.FrameType is ObuFrameType.IntraOnlyFrame or ObuFrameType.KeyFrame;
/// <summary>
/// Gets or sets the decoded and rendered frame dimensions.
/// </summary>
public ObuFrameSize FrameSize { get; set; } = new ObuFrameSize();
/// <summary>
/// Gets or sets the frame width in mode-information units.
/// </summary>
public int ModeInfoColumnCount { get; set; }
/// <summary>
/// Gets or sets the frame height in mode-information units.
/// </summary>
public int ModeInfoRowCount { get; set; }
/// <summary>
/// Gets or sets a value indicating whether an existing reference frame is displayed without decoding a new frame.
/// </summary>
public bool ShowExistingFrame { get; set; }
/// <summary>
/// Gets or sets the coded frame type.
/// </summary>
public ObuFrameType FrameType { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the decoded frame is immediately displayed.
/// </summary>
public bool ShowFrame { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the frame may be displayed by a later header.
/// </summary>
public bool ShowableFrame { get; set; }
/// <summary>
/// Gets or sets the reference map index selected when showing an existing frame.
/// </summary>
public uint FrameToShowMapIdx { get; set; }
/// <summary>
/// Gets or sets the display frame identifier.
/// </summary>
public uint DisplayFrameId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the frame can be decoded without state from earlier frames.
/// </summary>
public bool ErrorResilientMode { get; set; }
/// <summary>
/// Gets or sets a value indicating whether screen-content coding tools are permitted.
/// </summary>
public bool AllowScreenContentTools { get; set; }
/// <summary>
/// Gets or sets a value indicating whether probability adaptation is disabled for this frame.
/// </summary>
public bool DisableCdfUpdate { get; set; }
/// <summary>
/// Gets or sets the identifier of the current frame.
/// </summary>
public uint CurrentFrameId { get; set; }
/// <summary>
/// Gets or sets the frame order hint.
/// </summary>
public uint OrderHint { get; set; }
/// <summary>
/// Gets or sets the zero-based inter-reference type that supplies the initial frame context, or the no-reference
/// sentinel.
/// </summary>
public uint PrimaryReferenceFrame { get; set; } = Av1Constants.PrimaryReferenceFrameNone;
/// <summary>
/// Gets or sets the resolved reference-map slot supplying the initial frame context, or <see langword="null"/> when
/// the frame uses the default context.
/// </summary>
public byte? PrimaryReferenceSlot { get; set; }
/// <summary>
/// Gets or sets the bit mask of reference slots refreshed by this frame.
/// </summary>
public uint RefreshFrameFlags { get; set; }
/// <summary>
/// Gets or sets the presentation time signaled by temporal point information.
/// </summary>
public uint FramePresentationTime { get; set; }
/// <summary>
/// Gets the validity state of each reference-frame slot.
/// </summary>
/// <returns>The mutable eight-entry reference-validity table.</returns>
public Span<bool> GetReferenceValidity() => this.referenceValid;
/// <summary>
/// Gets the multi-bit order hint associated with each reference-frame slot.
/// </summary>
/// <returns>The mutable eight-entry reference-order-hint table.</returns>
public Span<uint> GetReferenceOrderHints() => this.referenceOrderHint;
/// <summary>
/// Gets the frame identifier associated with each reference-map slot.
/// </summary>
/// <returns>The mutable eight-entry reference-frame-identifier table.</returns>
public Span<uint> GetReferenceFrameIds() => this.referenceFrameId;
/// <summary>
/// Gets the reference-map slot selected for each inter reference type.
/// </summary>
/// <returns>The mutable seven-entry inter-reference-map table.</returns>
public Span<uint> GetReferenceFrameIndices() => this.referenceFrameIndex[..Av1Constants.ReferencesPerFrame];
/// <summary>
/// Gets the global-motion model associated with each canonical inter reference type from LAST through ALTREF.
/// </summary>
/// <returns>The mutable seven-entry global-motion parameter table.</returns>
public Span<Av1GlobalMotionParameters> GetGlobalMotionParameters() => this.globalMotionParameters;
/// <summary>
/// Invalidates retained reference slots whose frame identifiers fall outside the permitted backwards window.
/// </summary>
/// <param name="frameIdLength">The number of bits in the modulo frame-identifier domain.</param>
/// <param name="deltaFrameIdLength">The number of bits used to signal reference-frame identifier deltas.</param>
public void MarkReferenceFrames(int frameIdLength, int deltaFrameIdLength)
{
uint referenceWindow = 1U << deltaFrameIdLength;
uint frameIdModulus = 1U << frameIdLength;
Span<uint> referenceFrameIds = this.GetReferenceFrameIds();
Span<bool> referenceValidity = this.GetReferenceValidity();
for (int slot = 0; slot < Av1Constants.ReferenceFrameCount; slot++)
{
uint referenceFrameId = referenceFrameIds[slot];
if (this.CurrentFrameId > referenceWindow)
{
// Without wraparound, a retained identifier is valid only in the closed interval ending at the
// current identifier and extending referenceWindow values backwards.
if (referenceFrameId > this.CurrentFrameId || referenceFrameId < this.CurrentFrameId - referenceWindow)
{
referenceValidity[slot] = false;
}
}
else
{
// When the backwards window crosses zero, valid identifiers occupy both ends of the modulo domain.
// Only the open interval between the current identifier and the wrapped lower bound is invalid.
uint wrappedLowerBound = frameIdModulus + this.CurrentFrameId - referenceWindow;
if (referenceFrameId > this.CurrentFrameId && referenceFrameId < wrappedLowerBound)
{
referenceValidity[slot] = false;
}
}
}
}
/// <summary>
/// Provides inline storage for the seven canonical AV1 inter reference types.
/// </summary>
/// <typeparam name="T">The stored parameter type.</typeparam>
[InlineArray(Av1Constants.ReferencesPerFrame)]
private struct InlineArray7<T>
{
/// <summary>
/// The first element in the compiler-expanded inline buffer.
/// </summary>
private T element;
}
}

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

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

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

@ -0,0 +1,40 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the coded, upscaled, and rendered dimensions of an AV1 frame.
/// </summary>
internal sealed class ObuFrameSize
{
/// <summary>
/// Gets or sets the coded frame width.
/// </summary>
public int FrameWidth { get; set; }
/// <summary>
/// Gets or sets the coded frame height.
/// </summary>
public int FrameHeight { get; set; }
/// <summary>
/// Gets or sets the denominator used by AV1 super-resolution scaling.
/// </summary>
public int SuperResolutionDenominator { get; set; }
/// <summary>
/// Gets or sets the frame width after super-resolution upscaling.
/// </summary>
public int SuperResolutionUpscaledWidth { get; set; }
/// <summary>
/// Gets or sets the intended display width.
/// </summary>
public int RenderWidth { get; set; }
/// <summary>
/// Gets or sets the intended display height.
/// </summary>
public int RenderHeight { get; set; }
}

30
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameType.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Identifies the AV1 frame type signaled by a frame header.
/// </summary>
internal enum ObuFrameType
{
/// <summary>
/// A key frame that is decoded without reference to another frame.
/// </summary>
KeyFrame = 0,
/// <summary>
/// An inter frame that can refer to previously decoded frames.
/// </summary>
InterFrame = 1,
/// <summary>
/// An intra-only frame that does not refresh all reference slots.
/// </summary>
IntraOnlyFrame = 2,
/// <summary>
/// A switch frame that permits switching between coded sequences.
/// </summary>
SwitchFrame = 3,
}

45
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuHeader.cs

@ -0,0 +1,45 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the parsed header and payload size of an AV1 open bitstream unit.
/// </summary>
internal sealed class ObuHeader
{
/// <summary>
/// Gets or sets the number of bytes occupied by the fixed OBU header and optional extension.
/// </summary>
public int Size { get; set; }
/// <summary>
/// Gets or sets the OBU payload type.
/// </summary>
public ObuType Type { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the OBU carries an explicit payload-size field.
/// </summary>
public bool HasSize { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the OBU carries temporal and spatial identifiers.
/// </summary>
public bool HasExtension { get; set; }
/// <summary>
/// Gets or sets the temporal-layer identifier.
/// </summary>
public int TemporalId { get; set; }
/// <summary>
/// Gets or sets the spatial-layer identifier.
/// </summary>
public int SpatialId { get; set; }
/// <summary>
/// Gets or sets the OBU payload size, in bytes.
/// </summary>
public int PayloadSize { get; set; }
}

77
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopFilterParameters.cs

@ -0,0 +1,77 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the AV1 deblocking-loop-filter parameters for a frame.
/// </summary>
internal sealed class ObuLoopFilterParameters
{
/// <summary>
/// Stores the horizontal and vertical luma filter levels.
/// </summary>
private InlineArray4<int> filterLevel;
/// <summary>
/// Stores the fixed reference-frame delta table.
/// </summary>
private InlineArray8<int> referenceDeltas;
/// <summary>
/// Stores the fixed prediction-mode delta table.
/// </summary>
private InlineArray4<int> modeDeltas;
/// <summary>
/// Initializes a new instance of the <see cref="ObuLoopFilterParameters"/> class with the AV1 default reference and mode deltas.
/// </summary>
public ObuLoopFilterParameters()
{
// AV1 indexes this table from Intra through Alternate. Golden is -1; Backward remains 0.
this.referenceDeltas[0] = 1;
this.referenceDeltas[4] = -1;
this.referenceDeltas[6] = -1;
this.referenceDeltas[7] = -1;
}
/// <summary>
/// Gets the horizontal and vertical luma filter levels.
/// </summary>
public Span<int> FilterLevel => this.filterLevel[..2];
/// <summary>
/// Gets or sets the U-plane filter level.
/// </summary>
public int FilterLevelU { get; set; }
/// <summary>
/// Gets or sets the V-plane filter level.
/// </summary>
public int FilterLevelV { get; set; }
/// <summary>
/// Gets or sets the filter sharpness level.
/// </summary>
public int SharpnessLevel { get; set; }
/// <summary>
/// Gets or sets a value indicating whether reference-frame and mode deltas are enabled.
/// </summary>
public bool ReferenceDeltaModeEnabled { get; set; }
/// <summary>
/// Gets or sets a value indicating whether reference-frame and mode deltas are updated by this frame.
/// </summary>
public bool ReferenceDeltaModeUpdate { get; set; }
/// <summary>
/// Gets the filter-level deltas for the AV1 reference-frame categories.
/// </summary>
public Span<int> ReferenceDeltas => this.referenceDeltas;
/// <summary>
/// Gets the filter-level deltas for the AV1 prediction modes.
/// </summary>
public Span<int> ModeDeltas => this.modeDeltas[..2];
}

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

@ -0,0 +1,20 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the loop-restoration type and unit size for one color plane.
/// </summary>
internal struct ObuLoopRestorationItem
{
/// <summary>
/// Gets or sets the restoration-unit size, in samples.
/// </summary>
public int Size { get; set; }
/// <summary>
/// Gets or sets the restoration filter type.
/// </summary>
public ObuRestorationType Type { get; set; }
}

40
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationParameters.cs

@ -0,0 +1,40 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the AV1 loop-restoration configuration for a frame.
/// </summary>
internal sealed class ObuLoopRestorationParameters
{
/// <summary>
/// Stores the fixed three plane configurations without an outer array or per-plane object allocation.
/// </summary>
private InlineArray4<ObuLoopRestorationItem> items;
/// <summary>
/// Gets or sets a value indicating whether any plane uses loop restoration.
/// </summary>
public bool UsesLoopRestoration { get; set; }
/// <summary>
/// Gets or sets a value indicating whether either chroma plane uses loop restoration.
/// </summary>
public bool UsesChromaLoopRestoration { get; set; }
/// <summary>
/// Gets the loop-restoration configuration for each plane.
/// </summary>
public Span<ObuLoopRestorationItem> Items => this.items[..3];
/// <summary>
/// Gets or sets the luma restoration-unit size shift.
/// </summary>
public int UnitShift { get; set; }
/// <summary>
/// Gets or sets the chroma restoration-unit size shift relative to luma.
/// </summary>
public int UVShift { get; set; }
}

95
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs

@ -0,0 +1,95 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Identifies the CICP matrix coefficients used to derive luma and chroma components.
/// </summary>
internal enum ObuMatrixCoefficients
{
/// <summary>
/// The identity matrix used for GBR component ordering.
/// </summary>
Identity = 0,
/// <summary>
/// ITU-R BT.709 coefficients.
/// </summary>
Bt709 = 1,
/// <summary>
/// Unspecified coefficients.
/// </summary>
Unspecified = 2,
/// <summary>
/// United States FCC 73.628 coefficients.
/// </summary>
Fcc = 4,
/// <summary>
/// ITU-R BT.470 System B and G coefficients.
/// </summary>
Bt470BG = 5,
/// <summary>
/// ITU-R BT.601 coefficients.
/// </summary>
Bt601 = 6,
/// <summary>
/// SMPTE 240M coefficients.
/// </summary>
Smpte240 = 7,
/// <summary>
/// SMPTE YCgCo coefficients.
/// </summary>
SmpteYCgCo = 8,
/// <summary>
/// ITU-R BT.2020 non-constant-luminance coefficients.
/// </summary>
Bt2020NonConstantLuminance = 9,
/// <summary>
/// ITU-R BT.2020 constant-luminance coefficients.
/// </summary>
Bt2020ConstantLuminance = 10,
/// <summary>
/// SMPTE ST 2085 YDzDx coefficients.
/// </summary>
Smpte2085 = 11,
/// <summary>
/// Chromaticity-derived non-constant-luminance coefficients.
/// </summary>
ChromaticityDerivedNonConstantLuminance = 12,
/// <summary>
/// Chromaticity-derived constant-luminance coefficients.
/// </summary>
ChromaticityDerivedConstantLuminance = 13,
/// <summary>
/// ITU-R BT.2100 ICtCp coefficients.
/// </summary>
Bt2100ICtCp = 14,
/// <summary>
/// IPT-C2 coefficients.
/// </summary>
IptC2 = 15,
/// <summary>
/// Reversible YCgCo coefficients with two additional encoded bits.
/// </summary>
YCgCoRe = 16,
/// <summary>
/// Reversible YCgCo coefficients with one additional encoded bit.
/// </summary>
YCgCoRo = 17,
}

40
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMetadataType.cs

@ -0,0 +1,40 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Identifies the payload carried by an AV1 metadata OBU.
/// </summary>
internal enum ObuMetadataType
{
/// <summary>
/// The reserved zero value.
/// </summary>
Reserved = 0,
/// <summary>
/// Content light-level metadata.
/// </summary>
HdrCll = 1,
/// <summary>
/// Mastering-display color-volume metadata.
/// </summary>
HdrMdcv = 2,
/// <summary>
/// Scalability-structure metadata.
/// </summary>
Scalability = 3,
/// <summary>
/// ITU-T T.35 terminal-provider metadata.
/// </summary>
ItutT35 = 4,
/// <summary>
/// Timecode metadata.
/// </summary>
Timecode = 5,
}

46
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOperatingPoint.cs

@ -0,0 +1,46 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the sequence-level constraints for an AV1 operating point.
/// </summary>
internal sealed class ObuOperatingPoint
{
/// <summary>
/// Gets or sets the operating-point index.
/// </summary>
public int OperatorIndex { get; set; }
/// <summary>
/// Gets or sets the AV1 sequence-level index.
/// </summary>
public int SequenceLevelIndex { get; set; }
/// <summary>
/// Gets or sets the sequence tier.
/// </summary>
public int SequenceTier { get; set; }
/// <summary>
/// Gets or sets a value indicating whether decoder-model timing is present for this operating point.
/// </summary>
public bool IsDecoderModelInfoPresent { get; set; }
/// <summary>
/// Gets or sets a value indicating whether an initial display delay is present for this operating point.
/// </summary>
public bool IsInitialDisplayDelayPresent { get; set; }
/// <summary>
/// Gets or sets the initial display delay, in decoded frames.
/// </summary>
public uint InitialDisplayDelay { get; set; }
/// <summary>
/// Gets or sets the bitmask selecting temporal and spatial layers for the operating point.
/// A value of zero selects the complete coded sequence.
/// </summary>
public uint Idc { get; set; }
}

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

@ -0,0 +1,53 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the sequence-level order-hint and dependent prediction-tool settings.
/// </summary>
internal sealed class ObuOrderHintInfo
{
/// <summary>
/// Gets or sets a value indicating whether order hints are enabled.
/// </summary>
public bool EnableOrderHint { get; set; }
/// <summary>
/// Gets or sets a value indicating whether joint compound prediction is enabled.
/// </summary>
public bool EnableJointCompound { get; set; }
/// <summary>
/// Gets or sets a value indicating whether reference-frame motion vectors are enabled.
/// </summary>
public bool EnableReferenceFrameMotionVectors { get; set; }
/// <summary>
/// Gets or sets the number of bits used to encode order hints.
/// </summary>
public int OrderHintBits { get; set; }
/// <summary>
/// Computes the signed distance between two order hints in the sequence's modulo order-hint domain.
/// </summary>
/// <param name="first">The first order hint.</param>
/// <param name="second">The order hint subtracted from <paramref name="first"/>.</param>
/// <returns>
/// The shortest signed modulo distance, or zero when order hints are disabled for the sequence.
/// </returns>
public int GetRelativeDistance(uint first, uint second)
{
if (!this.EnableOrderHint)
{
return 0;
}
int difference = (int)first - (int)second;
int signBit = 1 << (this.OrderHintBits - 1);
// Folding around the sign bit maps the unsigned difference to [-2^(bits - 1), 2^(bits - 1)), including
// the wraparound between the highest encoded order hint and zero.
return (difference & (signBit - 1)) - (difference & signBit);
}
}

65
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuQuantizationParameters.cs

@ -0,0 +1,65 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the AV1 quantization parameters for a frame.
/// </summary>
internal sealed class ObuQuantizationParameters
{
/// <summary>
/// Stores the effective quantizer index for each of the eight segments without a per-header array allocation.
/// </summary>
private InlineArray8<int> qIndex;
/// <summary>
/// Stores the three plane DC quantizer-index deltas without a per-frame array allocation.
/// </summary>
private InlineArray4<int> deltaQDc;
/// <summary>
/// Stores the three plane AC quantizer-index deltas without a per-frame array allocation.
/// </summary>
private InlineArray4<int> deltaQAc;
/// <summary>
/// Stores the three plane quantization-matrix levels without a per-frame array allocation.
/// </summary>
private InlineArray4<int> qMatrix;
/// <summary>
/// Gets or sets the base quantizer index.
/// </summary>
public int BaseQIndex { get; set; }
/// <summary>
/// Gets the mutable effective quantizer indices for each segment.
/// </summary>
public Span<int> QIndex => this.qIndex;
/// <summary>
/// Gets or sets a value indicating whether quantization matrices are enabled.
/// </summary>
public bool IsUsingQMatrix { get; set; }
/// <summary>
/// Gets the DC quantizer-index deltas for the Y, U, and V planes.
/// </summary>
public Span<int> DeltaQDc => this.deltaQDc[..3];
/// <summary>
/// Gets the AC quantizer-index deltas for the Y, U, and V planes.
/// </summary>
public Span<int> DeltaQAc => this.deltaQAc[..3];
/// <summary>
/// Gets the quantization-matrix level for the Y, U, and V planes.
/// </summary>
public Span<int> QMatrix => this.qMatrix[..3];
/// <summary>
/// Gets or sets a value indicating whether the U and V planes use separate quantizer deltas.
/// </summary>
public bool HasSeparateUvDelta { get; set; }
}

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

File diff suppressed because it is too large

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

@ -0,0 +1,20 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Identifies whether inter blocks use only single references or can select compound references.
/// </summary>
internal enum ObuReferenceMode
{
/// <summary>
/// Only single-reference prediction is permitted.
/// </summary>
SingleReference = 0,
/// <summary>
/// Each eligible block selects single- or compound-reference prediction.
/// </summary>
ReferenceModeSelect = 1,
}

30
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuRestorationType.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Identifies the loop-restoration mode encoded by the two-bit frame-restoration syntax.
/// </summary>
internal enum ObuRestorationType : uint
{
/// <summary>
/// Loop restoration is disabled.
/// </summary>
None = 0,
/// <summary>
/// Each restoration unit selects its filter type.
/// </summary>
Switchable = 1,
/// <summary>
/// Separable Wiener filtering is used.
/// </summary>
Wiener = 2,
/// <summary>
/// Self-guided restoration projection is used.
/// </summary>
SgrProj = 3,
}

50
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSegmentationLevelFeature.cs

@ -0,0 +1,50 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Identifies a feature that AV1 can configure independently for each segment.
/// </summary>
internal enum ObuSegmentationLevelFeature
{
/// <summary>
/// Adjusts the segment quantizer index.
/// </summary>
AlternativeQuantizer,
/// <summary>
/// Adjusts the vertical luma loop-filter level.
/// </summary>
AlternativeLoopFilterYVertical,
/// <summary>
/// Adjusts the horizontal luma loop-filter level.
/// </summary>
AlternativeLoopFilterYHorizontal,
/// <summary>
/// Adjusts the U-plane loop-filter level.
/// </summary>
AlternativeLoopFilterU,
/// <summary>
/// Adjusts the V-plane loop-filter level.
/// </summary>
AlternativeLoopFilterV,
/// <summary>
/// Selects a reference frame for the segment.
/// </summary>
ReferenceFrame,
/// <summary>
/// Marks every block in the segment as skipped.
/// </summary>
Skip,
/// <summary>
/// Uses the global motion vector for the segment.
/// </summary>
GlobalMotionVector,
}

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

@ -0,0 +1,113 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the AV1 segmentation state and per-segment feature values for a frame.
/// </summary>
internal sealed class ObuSegmentationParameters
{
/// <summary>
/// Stores the feature-enable flags for each segment and feature without per-frame array allocations.
/// </summary>
private InlineArray8<InlineArray8<bool>> featureEnabled;
/// <summary>
/// Stores the feature values for each segment and feature without per-frame array allocations.
/// </summary>
private InlineArray8<InlineArray8<int>> featureData;
/// <summary>
/// Stores the effective quantization-matrix level for each plane and segment without jagged arrays.
/// </summary>
private InlineArray4<InlineArray8<int>> qmLevel;
/// <summary>
/// Gets the mutable effective quantization-matrix levels for the Y, U, and V planes.
/// </summary>
public Span<InlineArray8<int>> QMLevel => this.qmLevel[..Av1Constants.MaxPlanes];
/// <summary>
/// Gets or sets a value indicating whether segmentation is enabled for the frame.
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Gets or sets a value indicating whether segment identifiers are decoded before skip-mode decisions.
/// </summary>
public bool SegmentIdPrecedesSkip { get; set; }
/// <summary>
/// Gets or sets the greatest segment identifier that has at least one active feature.
/// </summary>
public int LastActiveSegmentId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the segmentation map is updated while decoding the frame.
/// </summary>
public int SegmentationUpdateMap { get; set; }
/// <summary>
/// Gets or sets a value indicating whether segmentation-map updates are coded relative to the existing map.
/// </summary>
public int SegmentationTemporalUpdate { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the frame supplies new per-segment feature data.
/// </summary>
public int SegmentationUpdateData { get; set; }
/// <summary>
/// Determines whether a feature is active for a segment.
/// </summary>
/// <param name="segmentId">The segment identifier.</param>
/// <param name="feature">The feature to inspect.</param>
/// <returns><see langword="true"/> when the feature is active; otherwise, <see langword="false"/>.</returns>
public bool IsFeatureActive(int segmentId, ObuSegmentationLevelFeature feature)
=> this.featureEnabled[segmentId][(int)feature];
/// <summary>
/// Gets a feature value for a segment.
/// </summary>
/// <param name="segmentId">The segment identifier.</param>
/// <param name="featureId">The feature identifier.</param>
/// <returns>The stored feature value.</returns>
public int GetFeatureData(int segmentId, int featureId) => this.featureData[segmentId][featureId];
/// <summary>
/// Sets whether a feature is active for a segment.
/// </summary>
/// <param name="segmentId">The segment identifier.</param>
/// <param name="featureId">The feature identifier.</param>
/// <param name="enabled">Whether the feature is active.</param>
public void SetFeatureEnabled(int segmentId, int featureId, bool enabled)
=> this.featureEnabled[segmentId][featureId] = enabled;
/// <summary>
/// Sets a feature value for a segment.
/// </summary>
/// <param name="segmentId">The segment identifier.</param>
/// <param name="featureId">The feature identifier.</param>
/// <param name="value">The feature value.</param>
public void SetFeatureData(int segmentId, int featureId, int value)
=> this.featureData[segmentId][featureId] = value;
/// <summary>
/// Replaces every feature enable flag and value with state from a primary reference frame.
/// </summary>
/// <param name="source">The primary-reference segmentation state.</param>
public void CopyFeaturesFrom(ObuSegmentationParameters source)
{
// AV1 inherits feature data but not the current frame's enabled or update flags. Both dimensions are fixed by
// the bitstream syntax, and copying values into this header prevents retained frames from sharing mutable state.
for (int segment = 0; segment < Av1Constants.MaxSegmentCount; segment++)
{
for (int feature = 0; feature < Av1Constants.SegmentationLevelMax; feature++)
{
this.featureEnabled[segment][feature] = source.featureEnabled[segment][feature];
this.featureData[segment][feature] = source.featureData[segment][feature];
}
}
}
}

229
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceHeader.cs

@ -0,0 +1,229 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Stores AV1 sequence capabilities, dimensions, timing, and color configuration.
/// </summary>
internal sealed class ObuSequenceHeader
{
/// <summary>
/// Backs <see cref="Use128x128Superblock"/> while its dependent geometry is updated.
/// </summary>
private bool use128x128Superblock;
/// <summary>
/// Initializes a new instance of the <see cref="ObuSequenceHeader"/> class with the normative 64x64 superblock geometry.
/// </summary>
public ObuSequenceHeader() => this.Use128x128Superblock = false;
/// <summary>
/// Gets or sets a value indicating whether filter-intra prediction is enabled.
/// </summary>
public bool EnableFilterIntra { get; set; }
/// <summary>
/// Gets or sets a value indicating whether constrained directional enhancement filtering is enabled.
/// </summary>
public bool EnableCdef { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the sequence contains one still picture.
/// </summary>
public bool IsStillPicture { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the reduced still-picture header syntax is used.
/// </summary>
public bool IsReducedStillPictureHeader { get; set; }
/// <summary>
/// Gets or sets the sequence profile.
/// </summary>
public ObuSequenceProfile SequenceProfile { get; set; }
/// <summary>
/// Gets or sets the signaled operating points.
/// </summary>
public ObuOperatingPoint[] OperatingPoint { get; set; } = new ObuOperatingPoint[1];
/// <summary>
/// Gets or sets the decoder-buffer model information when present.
/// </summary>
public ObuDecoderModelInfo? DecoderModelInfo { get; set; }
/// <summary>
/// Gets or sets a value indicating whether operating points may carry initial display delays.
/// </summary>
public bool InitialDisplayDelayPresentFlag { get; set; }
/// <summary>
/// Gets or sets a value indicating whether decoder-model information is present.
/// </summary>
public bool DecoderModelInfoPresentFlag { get; set; }
/// <summary>
/// Gets or sets a value indicating whether sequence timing information is present.
/// </summary>
public bool TimingInfoPresentFlag { get; set; }
/// <summary>
/// Gets or sets the sequence timing information when present.
/// </summary>
public ObuTimingInfo? TimingInfo { get; set; }
/// <summary>
/// Gets or sets a value indicating whether frame identifiers are signaled.
/// </summary>
public bool IsFrameIdNumbersPresent { get; set; }
/// <summary>
/// Gets or sets the number of bits used to signal frame widths minus one.
/// </summary>
public int FrameWidthBits { get; set; }
/// <summary>
/// Gets or sets the number of bits used to signal frame heights minus one.
/// </summary>
public int FrameHeightBits { get; set; }
/// <summary>
/// Gets or sets the maximum decoded frame width in pixels.
/// </summary>
public int MaxFrameWidth { get; set; }
/// <summary>
/// Gets or sets the maximum decoded frame height in pixels.
/// </summary>
public int MaxFrameHeight { get; set; }
/// <summary>
/// Gets or sets a value indicating whether 128x128 superblocks are used.
/// </summary>
public bool Use128x128Superblock
{
get => this.use128x128Superblock;
set
{
this.use128x128Superblock = value;
// Superblock selection controls every downstream partition-grid unit, so update the derived geometry atomically.
this.SuperblockSize = value ? Av1BlockSize.Block128x128 : Av1BlockSize.Block64x64;
this.SuperblockSizeLog2 = value ? 7 : 6;
this.SuperblockModeInfoSize = 1 << (this.SuperblockSizeLog2 - Av1Constants.ModeInfoSizeLog2);
}
}
/// <summary>
/// Gets the selected superblock dimensions.
/// </summary>
public Av1BlockSize SuperblockSize { get; private set; }
/// <summary>
/// Gets the superblock width and height in mode-information units.
/// </summary>
public int SuperblockModeInfoSize { get; private set; }
/// <summary>
/// Gets the base-two logarithm of the superblock size in pixels.
/// </summary>
public int SuperblockSizeLog2 { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether intra edge filtering is enabled.
/// </summary>
public bool EnableIntraEdgeFilter { get; set; }
/// <summary>
/// Gets or sets the order-hint capabilities.
/// </summary>
public ObuOrderHintInfo OrderHintInfo { get; set; } = new ObuOrderHintInfo();
/// <summary>
/// Gets or sets a value indicating whether order hints are enabled.
/// </summary>
public bool EnableOrderHint
{
get => this.OrderHintInfo.EnableOrderHint;
set
{
// Order-hint availability is consumed through OrderHintInfo by frame parsing, so
// keep the sequence-facing flag and dependent tool state synchronized.
this.OrderHintInfo.EnableOrderHint = value;
}
}
/// <summary>
/// Gets or sets a value indicating whether inter-intra compound prediction is enabled.
/// </summary>
public bool EnableInterIntraCompound { get; set; }
/// <summary>
/// Gets or sets a value indicating whether masked compound prediction is enabled.
/// </summary>
public bool EnableMaskedCompound { get; set; }
/// <summary>
/// Gets or sets a value indicating whether warped motion is enabled.
/// </summary>
public bool EnableWarpedMotion { get; set; }
/// <summary>
/// Gets or sets a value indicating whether independent horizontal and vertical interpolation filters are enabled.
/// </summary>
public bool EnableDualFilter { get; set; }
/// <summary>
/// Gets or sets the sequence-level integer-motion-vector selection.
/// </summary>
public int ForceIntegerMotionVector { get; set; }
/// <summary>
/// Gets or sets the sequence-level screen-content-tools selection.
/// </summary>
public int ForceScreenContentTools { get; set; }
/// <summary>
/// Gets or sets a value indicating whether frame super-resolution is enabled.
/// </summary>
public bool EnableSuperResolution { get; set; }
/// <summary>
/// Gets or sets a value indicating whether loop restoration is enabled.
/// </summary>
public bool EnableRestoration { get; set; }
/// <summary>
/// Gets or sets the decoded color configuration.
/// </summary>
public ObuColorConfig ColorConfig { get; set; } = new ObuColorConfig();
/// <summary>
/// Gets or sets a value indicating whether frame headers may carry film-grain parameters.
/// </summary>
public bool AreFilmGrainingParametersPresent { get; set; }
/// <summary>
/// Gets or sets the total number of bits in a frame identifier.
/// </summary>
public int FrameIdLength { get; set; }
/// <summary>
/// Gets or sets the number of bits in a delta frame identifier.
/// </summary>
public int DeltaFrameIdLength { get; set; }
/// <summary>
/// Gets or sets the additional frame-identifier bit count signaled by the sequence header.
/// </summary>
public uint AdditionalFrameIdLength { get; set; }
/// <summary>
/// Gets the decoder-buffer model information required by syntax whose presence flag is set.
/// </summary>
/// <returns>The decoder-buffer model information.</returns>
public ObuDecoderModelInfo GetDecoderModelInfo() =>
this.DecoderModelInfo
?? throw new InvalidOperationException("The AV1 sequence has no decoder-model information.");
}

25
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceProfile.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Identifies the AV1 sequence profile and its permitted bit-depth and chroma formats.
/// </summary>
internal enum ObuSequenceProfile : uint
{
/// <summary>
/// The Main profile.
/// </summary>
Main = 0,
/// <summary>
/// The High profile.
/// </summary>
High = 1,
/// <summary>
/// The Professional profile.
/// </summary>
Professional = 2,
}

109
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSkipModeParameters.cs

@ -0,0 +1,109 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the availability and enabled state of AV1 skip mode for a frame.
/// </summary>
internal sealed class ObuSkipModeParameters
{
/// <summary>
/// Gets a value indicating whether the frame is permitted to use skip mode.
/// </summary>
public bool SkipModeAllowed { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether skip mode is enabled for the frame.
/// </summary>
public bool SkipModeFlag { get; set; }
/// <summary>
/// Gets the first canonical inter-reference type selected for skip-mode blocks.
/// </summary>
public Av1ReferenceFrameType FirstReferenceFrame { get; private set; } = Av1ReferenceFrameType.None;
/// <summary>
/// Gets the second canonical inter-reference type selected for skip-mode blocks.
/// </summary>
public Av1ReferenceFrameType SecondReferenceFrame { get; private set; } = Av1ReferenceFrameType.None;
/// <summary>
/// Derives skip-mode availability and its reference pair from the current frame's retained-reference mapping.
/// </summary>
/// <param name="orderHintInfo">The sequence-level order-hint configuration.</param>
/// <param name="frameHeader">The current frame header and its seven canonical inter-reference mappings.</param>
public void Derive(ObuOrderHintInfo orderHintInfo, ObuFrameHeader frameHeader)
{
this.SkipModeAllowed = false;
this.FirstReferenceFrame = Av1ReferenceFrameType.None;
this.SecondReferenceFrame = Av1ReferenceFrameType.None;
if (!orderHintInfo.EnableOrderHint || frameHeader.IsIntra || frameHeader.ReferenceMode == ObuReferenceMode.SingleReference)
{
return;
}
ReadOnlySpan<uint> referenceFrameIndices = frameHeader.GetReferenceFrameIndices();
ReadOnlySpan<uint> referenceOrderHints = frameHeader.GetReferenceOrderHints();
int nearestForwardOrderHint = -1;
int nearestBackwardOrderHint = int.MaxValue;
int nearestForwardReferenceIndex = -1;
int nearestBackwardReferenceIndex = -1;
// The seven entries are canonical roles, while each value selects one physical reference-map slot. Compare
// the selected slot's order hint so duplicate roles retain the same deterministic ordering as the reference decoder.
for (int referenceIndex = 0; referenceIndex < Av1Constants.ReferencesPerFrame; referenceIndex++)
{
uint referenceOrderHint = referenceOrderHints[(int)referenceFrameIndices[referenceIndex]];
int distanceFromCurrent = orderHintInfo.GetRelativeDistance(referenceOrderHint, frameHeader.OrderHint);
if (distanceFromCurrent < 0 &&
(nearestForwardOrderHint == -1 || orderHintInfo.GetRelativeDistance(referenceOrderHint, (uint)nearestForwardOrderHint) > 0))
{
// Among past frames, the greatest relative order is the closest frame before the current one.
nearestForwardOrderHint = (int)referenceOrderHint;
nearestForwardReferenceIndex = referenceIndex;
}
else if (distanceFromCurrent > 0 &&
(nearestBackwardOrderHint == int.MaxValue || orderHintInfo.GetRelativeDistance(referenceOrderHint, (uint)nearestBackwardOrderHint) < 0))
{
// Among future frames, the smallest relative order is the closest frame after the current one.
nearestBackwardOrderHint = (int)referenceOrderHint;
nearestBackwardReferenceIndex = referenceIndex;
}
}
if (nearestForwardReferenceIndex >= 0 && nearestBackwardReferenceIndex < 0)
{
nearestBackwardOrderHint = -1;
// A forward-only sequence pairs the nearest past frame with the closest distinct frame preceding it.
for (int referenceIndex = 0; referenceIndex < Av1Constants.ReferencesPerFrame; referenceIndex++)
{
uint referenceOrderHint = referenceOrderHints[(int)referenceFrameIndices[referenceIndex]];
bool precedesNearestForward = orderHintInfo.GetRelativeDistance(referenceOrderHint, (uint)nearestForwardOrderHint) < 0;
if (precedesNearestForward &&
(nearestBackwardOrderHint == -1 || orderHintInfo.GetRelativeDistance(referenceOrderHint, (uint)nearestBackwardOrderHint) > 0))
{
nearestBackwardOrderHint = (int)referenceOrderHint;
nearestBackwardReferenceIndex = referenceIndex;
}
}
}
if (nearestForwardReferenceIndex < 0 || nearestBackwardReferenceIndex < 0)
{
return;
}
int firstReferenceIndex = Math.Min(nearestForwardReferenceIndex, nearestBackwardReferenceIndex);
int secondReferenceIndex = Math.Max(nearestForwardReferenceIndex, nearestBackwardReferenceIndex);
this.FirstReferenceFrame = (Av1ReferenceFrameType)(firstReferenceIndex + (int)Av1ReferenceFrameType.Last);
this.SecondReferenceFrame = (Av1ReferenceFrameType)(secondReferenceIndex + (int)Av1ReferenceFrameType.Last);
this.SkipModeAllowed = true;
}
}

107
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTileGroupHeader.cs

@ -0,0 +1,107 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the tile layout derived from an AV1 frame header.
/// </summary>
internal sealed class ObuTileGroupHeader
{
private InlineTileColumnBoundaryArray tileColumnStartModeInfo;
private InlineTileRowBoundaryArray tileRowStartModeInfo;
/// <summary>
/// Gets or sets the maximum tile width, in superblocks.
/// </summary>
public int MaxTileWidthSuperblock { get; set; }
/// <summary>
/// Gets or sets the maximum tile height, in superblocks.
/// </summary>
public int MaxTileHeightSuperblock { get; set; }
/// <summary>
/// Gets or sets the minimum base-2 logarithm of the tile-column count.
/// </summary>
public int MinLog2TileColumnCount { get; set; }
/// <summary>
/// Gets or sets the maximum base-2 logarithm of the tile-column count.
/// </summary>
public int MaxLog2TileColumnCount { get; set; }
/// <summary>
/// Gets or sets the maximum base-2 logarithm of the tile-row count.
/// </summary>
public int MaxLog2TileRowCount { get; set; }
/// <summary>
/// Gets or sets the minimum base-2 logarithm of the total tile count.
/// </summary>
public int MinLog2TileCount { get; set; }
/// <summary>
/// Gets or sets a value indicating whether tile columns and rows use uniform spacing.
/// </summary>
public bool HasUniformTileSpacing { get; set; }
/// <summary>
/// Gets or sets the base-2 logarithm of the tile-column count.
/// </summary>
public int TileColumnCountLog2 { get; set; }
/// <summary>
/// Gets or sets the number of tile columns.
/// </summary>
public int TileColumnCount { get; set; }
/// <summary>
/// Gets the fixed-capacity starting superblock column storage for each tile column.
/// </summary>
public Span<int> TileColumnStartModeInfo => this.tileColumnStartModeInfo;
/// <summary>
/// Gets or sets the minimum base-2 logarithm of the tile-row count.
/// </summary>
public int MinLog2TileRowCount { get; set; }
/// <summary>
/// Gets or sets the base-2 logarithm of the tile-row count.
/// </summary>
public int TileRowCountLog2 { get; set; }
/// <summary>
/// Gets the fixed-capacity starting superblock row storage for each tile row.
/// </summary>
public Span<int> TileRowStartModeInfo => this.tileRowStartModeInfo;
/// <summary>
/// Gets or sets the number of tile rows.
/// </summary>
public int TileRowCount { get; set; }
/// <summary>
/// Gets or sets the tile whose entropy context is retained after frame decoding.
/// </summary>
public uint ContextUpdateTileId { get; set; }
/// <summary>
/// Gets or sets the number of bytes used to signal each tile size.
/// </summary>
public int TileSizeBytes { get; set; }
[InlineArray(Av1Constants.MaxTileColumnCount + 1)]
private struct InlineTileColumnBoundaryArray
{
private int element;
}
[InlineArray(Av1Constants.MaxTileRowCount + 1)]
private struct InlineTileRowBoundaryArray
{
private int element;
}
}

36
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTimingInfo.cs

@ -0,0 +1,36 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Contains the timing syntax signaled by an AV1 sequence header.
/// </summary>
internal sealed class ObuTimingInfo
{
/// <summary>
/// Gets or sets NumUnitsInDisplayTick. NumUnitsInDisplayTick is the number of time units of a clock operating at the frequency TimeScale Hz that
/// corresponds to one increment of a clock tick counter. A display clock tick, in seconds, is equal to
/// NumUnitsInDisplayTick divided by TimeScale.
/// </summary>
public uint NumUnitsInDisplayTick { get; set; }
/// <summary>
/// Gets or sets TimeScale. TimeScale is the number of time units that pass in one second.
/// It is a requirement of bitstream conformance that TimeScale is greater than 0.
/// </summary>
public uint TimeScale { get; set; }
/// <summary>
/// Gets or sets a value indicating whether that pictures should be displayed according to their output order with the
/// number of ticks between two consecutive pictures (without dropping frames) specified by NumTicksPerPicture.
/// EqualPictureInterval equal to false indicates that the interval between two consecutive pictures is not specified.
/// </summary>
public bool EqualPictureInterval { get; set; }
/// <summary>
/// Gets or sets NumTicksPerPicture. NumTicksPerPicture specifies the number of clock ticks corresponding to output time between two
/// consecutive pictures in the output order.
/// </summary>
public uint NumTicksPerPicture { get; set; }
}

95
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTransferCharacteristics.cs

@ -0,0 +1,95 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Identifies the CICP transfer characteristics signaled by an AV1 sequence.
/// </summary>
internal enum ObuTransferCharacteristics
{
/// <summary>
/// ITU-R BT.709 transfer characteristics.
/// </summary>
Bt709 = 1,
/// <summary>
/// Unspecified transfer characteristics.
/// </summary>
Unspecified = 2,
/// <summary>
/// ITU-R BT.470 System M transfer characteristics.
/// </summary>
Bt470M = 4,
/// <summary>
/// ITU-R BT.470 System B and G transfer characteristics.
/// </summary>
Bt470BG = 5,
/// <summary>
/// ITU-R BT.601 transfer characteristics.
/// </summary>
Bt601 = 6,
/// <summary>
/// SMPTE 240M transfer characteristics.
/// </summary>
Smpte240 = 7,
/// <summary>
/// Linear light.
/// </summary>
Linear = 8,
/// <summary>
/// Logarithmic transfer with a 100:1 range.
/// </summary>
Log100 = 9,
/// <summary>
/// Logarithmic transfer with a 100 times square-root-of-ten to one range.
/// </summary>
Log100Sqrt10 = 10,
/// <summary>
/// IEC 61966-2-4 transfer characteristics.
/// </summary>
Iec61966 = 11,
/// <summary>
/// ITU-R BT.1361 transfer characteristics.
/// </summary>
Bt1361 = 12,
/// <summary>
/// IEC 61966-2-1 sRGB or sYCC transfer characteristics.
/// </summary>
Srgb = 13,
/// <summary>
/// ITU-R BT.2020 transfer characteristics for 10-bit systems.
/// </summary>
Bt202010Bit = 14,
/// <summary>
/// ITU-R BT.2020 transfer characteristics for 12-bit systems.
/// </summary>
Bt202012Bit = 15,
/// <summary>
/// SMPTE ST 2084 perceptual-quantizer transfer characteristics.
/// </summary>
Smpte2084 = 16,
/// <summary>
/// SMPTE ST 428 transfer characteristics.
/// </summary>
Smpte428 = 17,
/// <summary>
/// ITU-R BT.2100 hybrid-log-gamma transfer characteristics.
/// </summary>
Hlg = 18,
}

60
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuType.cs

@ -0,0 +1,60 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Identifies an AV1 open bitstream unit payload.
/// </summary>
internal enum ObuType
{
/// <summary>
/// The reserved zero value.
/// </summary>
None = 0,
/// <summary>
/// A sequence header.
/// </summary>
SequenceHeader = 1,
/// <summary>
/// A temporal delimiter.
/// </summary>
TemporalDelimiter = 2,
/// <summary>
/// A frame header without tile data.
/// </summary>
FrameHeader = 3,
/// <summary>
/// One or more encoded tiles.
/// </summary>
TileGroup = 4,
/// <summary>
/// Metadata associated with the coded sequence.
/// </summary>
Metadata = 5,
/// <summary>
/// A frame header followed by tile data.
/// </summary>
Frame = 6,
/// <summary>
/// A repeated copy of the current frame header.
/// </summary>
RedundantFrameHeader = 7,
/// <summary>
/// A list of tiles for large-scale tile decoding.
/// </summary>
TileList = 8,
/// <summary>
/// Padding bytes.
/// </summary>
Padding = 15,
}

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

@ -0,0 +1,935 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary>
/// Writes the AV1 open bitstream units required for a single still-image frame.
/// </summary>
internal class ObuWriter
{
/// <summary>
/// Writes a temporal delimiter and the supplied sequence and frame OBUs.
/// </summary>
/// <param name="configuration">The configuration used to allocate temporary encoding memory.</param>
/// <param name="stream">The destination stream.</param>
/// <param name="sequenceHeader">The optional still-picture sequence header.</param>
/// <param name="frameHeader">The optional intra-frame header.</param>
/// <param name="tileWriter">The tile writer used when a frame header is supplied.</param>
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Performance",
"CA1822:Mark members as static",
Justification = "Preserves the existing writer instance contract.")]
public void WriteAll(Configuration configuration, Stream stream, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, IAv1TileWriter tileWriter)
{
// The allocation expands when necessary; this initial size avoids repeated growth for
// the small headers and tiles produced by the current still-image encoder.
int initialBufferSize = 2000;
AutoExpandingMemory<byte> buffer = new(configuration, initialBufferSize);
Av1BitStreamWriter writer = new(buffer);
WriteObuHeaderAndSize(stream, ObuType.TemporalDelimiter, []);
if (sequenceHeader != null)
{
WriteSequenceHeader(ref writer, sequenceHeader);
int bytesWritten = (writer.BitPosition + 7) >> 3;
writer.Flush();
WriteObuHeaderAndSize(stream, ObuType.SequenceHeader, buffer.GetSpan(bytesWritten));
}
if (frameHeader != null && sequenceHeader != null)
{
WriteFrameHeader(ref writer, sequenceHeader, frameHeader);
if (frameHeader.TilesInfo != null)
{
WriteTileGroup(ref writer, frameHeader.TilesInfo, tileWriter);
}
int bytesWritten = (writer.BitPosition + 7) >> 3;
writer.Flush();
WriteObuHeaderAndSize(stream, ObuType.Frame, buffer.GetSpan(bytesWritten));
}
}
/// <summary>
/// Creates a byte-aligned OBU header with an explicit payload-size field and no extension.
/// </summary>
/// <param name="type">The OBU payload type.</param>
/// <returns>The encoded OBU header byte.</returns>
private static byte WriteObuHeader(ObuType type)
{
// The only set fields are the four-bit type and the has-size flag; forbidden,
// extension, and reserved bits remain zero.
return (byte)(((byte)type << 3) | 0x02);
}
/// <summary>
/// Writes a complete byte-aligned OBU with a little-endian base-128 payload size.
/// </summary>
/// <param name="stream">The destination stream.</param>
/// <param name="type">The OBU payload type.</param>
/// <param name="payload">The complete OBU payload.</param>
private static void WriteObuHeaderAndSize(Stream stream, ObuType type, ReadOnlySpan<byte> payload)
{
stream.WriteByte(WriteObuHeader(type));
// A 32-bit OBU payload length requires at most five base-128 bytes.
Span<byte> lengthBytes = stackalloc byte[5];
int lengthLength = Av1BitStreamWriter.GetLittleEndianBytes128((uint)payload.Length, lengthBytes);
stream.Write(lengthBytes, 0, lengthLength);
stream.Write(payload);
}
/// <summary>
/// Writes a trailing one bit followed by enough zero bits to reach a byte boundary.
/// </summary>
/// <param name="writer">The bit writer receiving the trailing bits.</param>
/// <remarks>Writes an additional byte when the writer is already byte aligned.</remarks>
private static void WriteTrailingBits(ref Av1BitStreamWriter writer)
{
int bitsBeforeAlignment = 8 - (writer.BitPosition & 0x7);
writer.WriteLiteral(1U << (bitsBeforeAlignment - 1), bitsBeforeAlignment);
}
/// <summary>
/// Writes zero padding until the output reaches a byte boundary.
/// </summary>
/// <param name="writer">The bit writer to align.</param>
private static void AlignToByteBoundary(ref Av1BitStreamWriter writer)
{
while ((writer.BitPosition & 0x7) > 0)
{
writer.WriteBoolean(false);
}
}
/// <summary>
/// Writes a reduced still-picture sequence header.
/// </summary>
/// <param name="writer">The bit writer receiving the sequence header.</param>
/// <param name="sequenceHeader">The sequence header to encode.</param>
private static void WriteSequenceHeader(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader)
{
writer.WriteLiteral((uint)sequenceHeader.SequenceProfile, 3);
writer.WriteBoolean(true); // IsStillPicture
writer.WriteBoolean(true); // IsReducedStillPicture
writer.WriteLiteral((uint)sequenceHeader.OperatingPoint[0].SequenceLevelIndex, Av1Constants.LevelBits);
// Frame width and Height
writer.WriteLiteral((uint)sequenceHeader.FrameWidthBits - 1, 4);
writer.WriteLiteral((uint)sequenceHeader.FrameHeightBits - 1, 4);
writer.WriteLiteral((uint)sequenceHeader.MaxFrameWidth - 1, sequenceHeader.FrameWidthBits);
writer.WriteLiteral((uint)sequenceHeader.MaxFrameHeight - 1, sequenceHeader.FrameHeightBits);
// Video related flags removed
writer.WriteBoolean(sequenceHeader.Use128x128Superblock);
writer.WriteBoolean(sequenceHeader.EnableFilterIntra);
writer.WriteBoolean(sequenceHeader.EnableIntraEdgeFilter);
// Video related flags removed
writer.WriteBoolean(sequenceHeader.EnableSuperResolution);
writer.WriteBoolean(sequenceHeader.EnableCdef);
writer.WriteBoolean(sequenceHeader.EnableRestoration);
WriteColorConfig(ref writer, sequenceHeader);
writer.WriteBoolean(sequenceHeader.AreFilmGrainingParametersPresent);
WriteTrailingBits(ref writer);
}
/// <summary>
/// Writes the sequence color configuration.
/// </summary>
/// <param name="writer">The bit writer receiving the color configuration.</param>
/// <param name="sequenceHeader">The sequence header containing the color configuration.</param>
private static void WriteColorConfig(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader)
{
ObuColorConfig colorConfig = sequenceHeader.ColorConfig;
WriteBitDepth(ref writer, colorConfig, sequenceHeader);
if (sequenceHeader.SequenceProfile != ObuSequenceProfile.High)
{
writer.WriteBoolean(colorConfig.IsMonochrome);
}
writer.WriteBoolean(colorConfig.IsColorDescriptionPresent);
if (colorConfig.IsColorDescriptionPresent)
{
writer.WriteLiteral((uint)colorConfig.ColorPrimaries, 8);
writer.WriteLiteral((uint)colorConfig.TransferCharacteristics, 8);
writer.WriteLiteral((uint)colorConfig.MatrixCoefficients, 8);
}
if (colorConfig.IsMonochrome)
{
writer.WriteBoolean(colorConfig.ColorRange);
return;
}
else if (
colorConfig.ColorPrimaries == ObuColorPrimaries.Bt709 &&
colorConfig.TransferCharacteristics == ObuTransferCharacteristics.Srgb &&
colorConfig.MatrixCoefficients == ObuMatrixCoefficients.Identity)
{
// AV1 fixes this RGB identity-matrix combination to full-range 4:4:4 and omits
// the range and subsampling fields used by YUV configurations.
colorConfig.ColorRange = true;
colorConfig.SubSamplingX = false;
colorConfig.SubSamplingY = false;
}
else
{
writer.WriteBoolean(colorConfig.ColorRange);
if (sequenceHeader.SequenceProfile == ObuSequenceProfile.Professional && colorConfig.BitDepth == Av1BitDepth.TwelveBit)
{
writer.WriteBoolean(colorConfig.SubSamplingX);
if (colorConfig.SubSamplingX)
{
writer.WriteBoolean(colorConfig.SubSamplingY);
}
}
if (colorConfig.SubSamplingX && colorConfig.SubSamplingY)
{
writer.WriteLiteral((uint)colorConfig.ChromaSamplePosition, 2);
}
}
writer.WriteBoolean(colorConfig.HasSeparateUvDelta);
}
/// <summary>
/// Writes the profile-dependent bit-depth flags.
/// </summary>
/// <param name="writer">The bit writer receiving the flags.</param>
/// <param name="colorConfig">The color configuration containing the bit depth.</param>
/// <param name="sequenceHeader">The sequence header containing the selected profile.</param>
private static void WriteBitDepth(ref Av1BitStreamWriter writer, ObuColorConfig colorConfig, ObuSequenceHeader sequenceHeader)
{
bool hasHighBitDepth = colorConfig.BitDepth > Av1BitDepth.EightBit;
writer.WriteBoolean(hasHighBitDepth);
if (sequenceHeader.SequenceProfile == ObuSequenceProfile.Professional && hasHighBitDepth)
{
writer.WriteBoolean(colorConfig.BitDepth == Av1BitDepth.TwelveBit);
}
}
/// <summary>
/// Writes the super-resolution enablement and scale denominator.
/// </summary>
/// <param name="writer">The bit writer receiving the super-resolution syntax.</param>
/// <param name="sequenceHeader">The sequence header controlling super-resolution availability.</param>
/// <param name="frameHeader">The frame header containing the scale denominator.</param>
private static void WriteSuperResolutionParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader)
{
bool useSuperResolution = sequenceHeader.EnableSuperResolution &&
frameHeader.FrameSize.SuperResolutionDenominator != Av1Constants.ScaleNumerator;
if (sequenceHeader.EnableSuperResolution)
{
writer.WriteBoolean(useSuperResolution);
}
if (useSuperResolution)
{
writer.WriteLiteral((uint)frameHeader.FrameSize.SuperResolutionDenominator - Av1Constants.SuperResolutionScaleDenominatorMinimum, Av1Constants.SuperResolutionScaleBits);
}
}
/// <summary>
/// Writes the render-size override when display dimensions differ from the upscaled frame.
/// </summary>
/// <param name="writer">The bit writer receiving the render-size syntax.</param>
/// <param name="frameHeader">The frame header containing coded and render dimensions.</param>
private static void WriteRenderSize(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader)
{
bool renderSizeAndFrameSizeDifferent =
frameHeader.FrameSize.RenderWidth != frameHeader.FrameSize.SuperResolutionUpscaledWidth ||
frameHeader.FrameSize.RenderHeight != frameHeader.FrameSize.FrameHeight;
writer.WriteBoolean(renderSizeAndFrameSizeDifferent);
if (renderSizeAndFrameSizeDifferent)
{
writer.WriteLiteral((uint)frameHeader.FrameSize.RenderWidth - 1, 16);
writer.WriteLiteral((uint)frameHeader.FrameSize.RenderHeight - 1, 16);
}
}
/// <summary>
/// Writes an optional frame-size override followed by super-resolution syntax.
/// </summary>
/// <param name="writer">The bit writer receiving the frame size.</param>
/// <param name="sequenceHeader">The sequence header defining dimension field widths.</param>
/// <param name="frameHeader">The frame header containing the dimensions.</param>
/// <param name="frameSizeOverrideFlag">A value indicating whether explicit dimensions are written.</param>
private static void WriteFrameSize(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, bool frameSizeOverrideFlag)
{
if (frameSizeOverrideFlag)
{
writer.WriteLiteral((uint)frameHeader.FrameSize.FrameWidth - 1, sequenceHeader.FrameWidthBits);
writer.WriteLiteral((uint)frameHeader.FrameSize.FrameHeight - 1, sequenceHeader.FrameHeightBits);
}
WriteSuperResolutionParameters(ref writer, sequenceHeader, frameHeader);
}
/// <summary>
/// Writes the frame tile layout.
/// </summary>
/// <param name="writer">The bit writer receiving the tile information.</param>
/// <param name="sequenceHeader">The sequence header defining superblock geometry.</param>
/// <param name="frameHeader">The frame header containing tile boundaries.</param>
private static void WriteTileInfo(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader)
{
ObuTileGroupHeader tileInfo = frameHeader.TilesInfo;
int superblockColumnCount;
int superblockRowCount;
int superblockSizeLog2 = sequenceHeader.SuperblockSizeLog2;
int superblockShift = superblockSizeLog2 - Av1Constants.ModeInfoSizeLog2;
superblockColumnCount = (frameHeader.ModeInfoColumnCount + sequenceHeader.SuperblockModeInfoSize - 1) >> superblockShift;
superblockRowCount = (frameHeader.ModeInfoRowCount + sequenceHeader.SuperblockModeInfoSize - 1) >> superblockShift;
int superBlockSize = superblockShift + 2;
int maxTileAreaOfSuperBlock = Av1Constants.MaxTileArea >> (2 * superBlockSize);
tileInfo.MaxTileWidthSuperblock = Av1Constants.MaxTileWidth >> superBlockSize;
tileInfo.MaxTileHeightSuperblock = (Av1Constants.MaxTileArea / Av1Constants.MaxTileWidth) >> superBlockSize;
tileInfo.MinLog2TileColumnCount = ObuReader.TileLog2(tileInfo.MaxTileWidthSuperblock, superblockColumnCount);
tileInfo.MaxLog2TileColumnCount = ObuReader.TileLog2(1, Math.Min(superblockColumnCount, Av1Constants.MaxTileColumnCount));
tileInfo.MaxLog2TileRowCount = ObuReader.TileLog2(1, Math.Min(superblockRowCount, Av1Constants.MaxTileRowCount));
tileInfo.MinLog2TileCount = Math.Max(tileInfo.MinLog2TileColumnCount, ObuReader.TileLog2(maxTileAreaOfSuperBlock, superblockColumnCount * superblockRowCount));
int log2TileColumnCount = ObuReader.TileLog2(1, tileInfo.TileColumnCount);
int log2TileRowCount = ObuReader.TileLog2(1, tileInfo.TileRowCount);
tileInfo.TileColumnCountLog2 = log2TileColumnCount;
tileInfo.TileRowCountLog2 = log2TileRowCount;
writer.WriteBoolean(tileInfo.HasUniformTileSpacing);
if (tileInfo.HasUniformTileSpacing)
{
// Uniform spaced tiles with power-of-two number of rows and columns
// tile columns
int ones = log2TileColumnCount - tileInfo.MinLog2TileColumnCount;
while (ones-- > 0)
{
writer.WriteBoolean(true);
}
if (log2TileColumnCount < tileInfo.MaxLog2TileColumnCount)
{
writer.WriteBoolean(false);
}
// rows
tileInfo.MinLog2TileRowCount = Math.Max(tileInfo.MinLog2TileCount - log2TileColumnCount, 0);
ones = log2TileRowCount - tileInfo.MinLog2TileRowCount;
while (ones-- > 0)
{
writer.WriteBoolean(true);
}
if (log2TileRowCount < tileInfo.MaxLog2TileRowCount)
{
writer.WriteBoolean(false);
}
}
else
{
int startSuperBlock = 0;
int i = 0;
for (; startSuperBlock < superblockColumnCount; i++)
{
uint widthInSuperBlocks = (uint)((tileInfo.TileColumnStartModeInfo[i] >> superblockShift) - startSuperBlock);
uint maxWidth = (uint)Math.Min(superblockColumnCount - startSuperBlock, tileInfo.MaxTileWidthSuperblock);
writer.WriteNonSymmetric(widthInSuperBlocks - 1, maxWidth);
startSuperBlock += (int)widthInSuperBlocks;
}
if (startSuperBlock != superblockColumnCount)
{
throw new ImageFormatException("Super block tiles width does not add up to total width.");
}
startSuperBlock = 0;
for (i = 0; startSuperBlock < superblockRowCount; i++)
{
uint heightInSuperBlocks = (uint)((tileInfo.TileRowStartModeInfo[i] >> superblockShift) - startSuperBlock);
uint maxHeight = (uint)Math.Min(superblockRowCount - startSuperBlock, tileInfo.MaxTileHeightSuperblock);
writer.WriteNonSymmetric(heightInSuperBlocks - 1, maxHeight);
startSuperBlock += (int)heightInSuperBlocks;
}
if (startSuperBlock != superblockRowCount)
{
throw new ImageFormatException("Super block tiles height does not add up to total height.");
}
}
if (tileInfo.TileColumnCountLog2 > 0 || tileInfo.TileRowCountLog2 > 0)
{
writer.WriteLiteral(tileInfo.ContextUpdateTileId, tileInfo.TileRowCountLog2 + tileInfo.TileColumnCountLog2);
writer.WriteLiteral((uint)tileInfo.TileSizeBytes - 1, 2);
}
frameHeader.TilesInfo = tileInfo;
}
/// <summary>
/// Writes the reduced uncompressed header for an intra still-image frame.
/// </summary>
/// <param name="writer">The bit writer receiving the uncompressed frame header.</param>
/// <param name="sequenceHeader">The sequence header controlling available coding tools.</param>
/// <param name="frameHeader">The frame header to encode.</param>
private static void WriteUncompressedFrameHeader(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader)
{
writer.WriteBoolean(frameHeader.DisableCdfUpdate);
if (sequenceHeader.ForceScreenContentTools == 2)
{
writer.WriteBoolean(frameHeader.AllowScreenContentTools);
}
else
{
// Guard.IsTrue(frameHeader.AllowScreenContentTools == sequenceHeader.ForceScreenContentTools);
}
if (frameHeader.AllowScreenContentTools)
{
if (sequenceHeader.ForceIntegerMotionVector == 2)
{
writer.WriteBoolean(frameHeader.ForceIntegerMotionVector);
}
else
{
// Guard.IsTrue(frameHeader.ForceIntegerMotionVector == sequenceHeader.ForceIntegerMotionVector, nameof(frameHeader.ForceIntegerMotionVector), "Frame and sequence must be in sync");
}
}
if (frameHeader.FrameType == ObuFrameType.KeyFrame)
{
if (!frameHeader.ShowFrame)
{
throw new NotImplementedException("No support for hidden frames.");
}
}
else if (frameHeader.FrameType == ObuFrameType.IntraOnlyFrame)
{
throw new NotImplementedException("No IntraOnly frames supported.");
}
if (frameHeader.FrameType == ObuFrameType.KeyFrame)
{
WriteFrameSize(ref writer, sequenceHeader, frameHeader, false);
WriteRenderSize(ref writer, frameHeader);
if (frameHeader.AllowScreenContentTools)
{
writer.WriteBoolean(frameHeader.AllowIntraBlockCopy);
}
}
else if (frameHeader.FrameType == ObuFrameType.IntraOnlyFrame)
{
WriteFrameSize(ref writer, sequenceHeader, frameHeader, false);
WriteRenderSize(ref writer, frameHeader);
if (frameHeader.AllowScreenContentTools)
{
writer.WriteBoolean(frameHeader.AllowIntraBlockCopy);
}
}
else
{
throw new NotImplementedException("Inter frames not applicable for AVIF.");
}
WriteTileInfo(ref writer, sequenceHeader, frameHeader);
WriteQuantizationParameters(ref writer, sequenceHeader, frameHeader);
WriteSegmentationParameters(ref writer, frameHeader);
Av1QuantizationLookup.UpdateFrameQuantizationState(frameHeader);
if (frameHeader.QuantizationParameters.BaseQIndex > 0)
{
writer.WriteBoolean(frameHeader.DeltaQParameters.IsPresent);
if (frameHeader.DeltaQParameters.IsPresent)
{
writer.WriteLiteral((uint)Av1Math.MostSignificantBit((uint)frameHeader.DeltaQParameters.Resolution), 2);
if (frameHeader.AllowIntraBlockCopy)
{
Guard.IsFalse(
frameHeader.DeltaLoopFilterParameters.IsPresent,
nameof(frameHeader.DeltaLoopFilterParameters.IsPresent),
"Allow INTRA block copy required Loop Filter.");
}
else
{
writer.WriteBoolean(frameHeader.DeltaLoopFilterParameters.IsPresent);
}
if (frameHeader.DeltaLoopFilterParameters.IsPresent)
{
writer.WriteLiteral((uint)Av1Math.MostSignificantBit((uint)frameHeader.DeltaLoopFilterParameters.Resolution), 2);
writer.WriteBoolean(frameHeader.DeltaLoopFilterParameters.IsMulti);
}
}
}
if (!frameHeader.AllLossless)
{
if (!frameHeader.CodedLossless)
{
WriteLoopFilterParameters(ref writer, sequenceHeader, frameHeader);
if (sequenceHeader.EnableCdef)
{
WriteCdefParameters(ref writer, sequenceHeader, frameHeader);
}
}
if (sequenceHeader.EnableRestoration)
{
WriteLoopRestorationParameters(ref writer, sequenceHeader, frameHeader);
}
}
// No Frame Reference mode selection for AVIF
WriteTransformMode(ref writer, frameHeader);
// No compound INTER-INTER for AVIF.
WriteFrameReferenceMode(ref writer, frameHeader);
WriteSkipModeParameters(ref writer, frameHeader);
// No warp motion for AVIF.
writer.WriteBoolean(frameHeader.UseReducedTransformSet);
WriteGlobalMotionParameters(ref writer, frameHeader);
WriteFilmGrainFilterParameters(ref writer, sequenceHeader, frameHeader);
}
/// <summary>
/// Writes the frame-header portion of a combined frame OBU.
/// </summary>
/// <param name="writer">The bit writer receiving the frame header.</param>
/// <param name="sequenceHeader">The sequence header controlling available coding tools.</param>
/// <param name="frameHeader">The frame header to encode.</param>
private static void WriteFrameHeader(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader)
{
WriteUncompressedFrameHeader(ref writer, sequenceHeader, frameHeader);
}
/// <summary>
/// Writes a tile-group header and all tile payloads for a combined frame OBU.
/// </summary>
/// <param name="writer">The bit writer receiving the tile group.</param>
/// <param name="tileInfo">The frame tile layout.</param>
/// <param name="tileWriter">The writer that produces each entropy-coded tile payload.</param>
private static void WriteTileGroup(ref Av1BitStreamWriter writer, ObuTileGroupHeader tileInfo, IAv1TileWriter tileWriter)
{
int tileCount = tileInfo.TileColumnCount * tileInfo.TileRowCount;
if (tileCount > 1)
{
// A combined OBU_FRAME has implicit complete-frame tile bounds. The reference decoder still
// writes the presence bit for a multi-tile frame, but requires that bit to remain zero.
writer.WriteBoolean(false);
}
AlignToByteBoundary(ref writer);
WriteTileData(ref writer, tileInfo, tileWriter);
}
/// <summary>
/// Writes the size-prefixed tile payloads in raster order.
/// </summary>
/// <param name="writer">The byte-aligned bit writer receiving tile data.</param>
/// <param name="tileInfo">The frame tile layout and tile-size field width.</param>
/// <param name="tileWriter">The writer that produces each tile payload.</param>
private static void WriteTileData(ref Av1BitStreamWriter writer, ObuTileGroupHeader tileInfo, IAv1TileWriter tileWriter)
{
int tileCount = tileInfo.TileColumnCount * tileInfo.TileRowCount;
for (int tileNum = 0; tileNum < tileCount; tileNum++)
{
Span<byte> tileData = tileWriter.WriteTile(tileNum);
if (tileNum != tileCount - 1 && tileCount > 1)
{
writer.WriteLittleEndian((uint)tileData.Length - 1U, tileInfo.TileSizeBytes);
}
writer.WriteBlob(tileData);
}
}
/// <summary>
/// Writes an optional signed quantizer-index delta.
/// </summary>
/// <param name="writer">The bit writer receiving the delta.</param>
/// <param name="deltaQ">The quantizer-index delta.</param>
private static void WriteDeltaQ(ref Av1BitStreamWriter writer, int deltaQ)
{
bool isCoded = deltaQ != 0;
writer.WriteBoolean(isCoded);
if (isCoded)
{
writer.WriteSignedFromUnsigned(deltaQ, 7);
}
}
/// <summary>
/// Writes the base index, plane deltas, and optional quantization matrices for a frame.
/// </summary>
/// <param name="writer">The bit writer receiving the quantization parameters.</param>
/// <param name="sequenceHeader">The sequence header defining active color planes.</param>
/// <param name="frameHeader">The frame header containing the quantization parameters.</param>
private static void WriteQuantizationParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader)
{
ObuQuantizationParameters quantParams = frameHeader.QuantizationParameters;
writer.WriteLiteral((uint)quantParams.BaseQIndex, 8);
WriteDeltaQ(ref writer, quantParams.DeltaQDc[(int)Av1Plane.Y]);
if (sequenceHeader.ColorConfig.PlaneCount > 1)
{
if (sequenceHeader.ColorConfig.HasSeparateUvDelta)
{
writer.WriteBoolean(quantParams.HasSeparateUvDelta);
}
WriteDeltaQ(ref writer, quantParams.DeltaQDc[(int)Av1Plane.U]);
WriteDeltaQ(ref writer, quantParams.DeltaQAc[(int)Av1Plane.U]);
if (quantParams.HasSeparateUvDelta)
{
WriteDeltaQ(ref writer, quantParams.DeltaQDc[(int)Av1Plane.V]);
WriteDeltaQ(ref writer, quantParams.DeltaQAc[(int)Av1Plane.V]);
}
}
writer.WriteBoolean(quantParams.IsUsingQMatrix);
if (quantParams.IsUsingQMatrix)
{
writer.WriteLiteral((uint)quantParams.QMatrix[(int)Av1Plane.Y], 4);
writer.WriteLiteral((uint)quantParams.QMatrix[(int)Av1Plane.U], 4);
if (sequenceHeader.ColorConfig.HasSeparateUvDelta)
{
writer.WriteLiteral((uint)quantParams.QMatrix[(int)Av1Plane.V], 4);
}
}
}
/// <summary>
/// Writes segmentation feature data for an independently decoded still-image frame.
/// </summary>
/// <param name="writer">The bit writer receiving the segmentation parameters.</param>
/// <param name="frameHeader">The frame header containing segmentation feature data.</param>
private static void WriteSegmentationParameters(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader)
{
ObuSegmentationParameters segmentation = frameHeader.SegmentationParameters;
writer.WriteBoolean(segmentation.Enabled);
if (!segmentation.Enabled)
{
return;
}
// The still-image writer emits independent intra frames with no primary reference.
// AV1 therefore infers update-map and update-data as enabled and carries feature data
// directly, without the inter-frame update flags.
for (int segmentId = 0; segmentId < Av1Constants.MaxSegmentCount; segmentId++)
{
for (int featureId = 0; featureId < Av1Constants.SegmentationLevelMax; featureId++)
{
bool enabled = segmentation.IsFeatureActive(segmentId, (ObuSegmentationLevelFeature)featureId);
writer.WriteBoolean(enabled);
if (!enabled)
{
continue;
}
int bitCount = Av1Constants.SegmentationFeatureBits[featureId];
int value = segmentation.GetFeatureData(segmentId, featureId);
if (Av1Constants.SegmentationFeatureSigned[featureId] == 1)
{
writer.WriteSignedFromUnsigned(value, bitCount + 1);
}
else
{
writer.WriteLiteral((uint)value, bitCount);
}
}
}
}
/// <summary>
/// Writes the deblocking-loop-filter levels and optional reference and mode deltas.
/// </summary>
/// <param name="writer">The bit writer receiving the loop-filter parameters.</param>
/// <param name="sequenceHeader">The sequence header defining active color planes.</param>
/// <param name="frameHeader">The frame header containing the loop-filter parameters.</param>
private static void WriteLoopFilterParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader)
{
if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy)
{
return;
}
writer.WriteLiteral((uint)frameHeader.LoopFilterParameters.FilterLevel[0], 6);
writer.WriteLiteral((uint)frameHeader.LoopFilterParameters.FilterLevel[1], 6);
if (sequenceHeader.ColorConfig.PlaneCount > 1)
{
if (frameHeader.LoopFilterParameters.FilterLevel[0] > 0 || frameHeader.LoopFilterParameters.FilterLevel[1] > 0)
{
writer.WriteLiteral((uint)frameHeader.LoopFilterParameters.FilterLevelU, 6);
writer.WriteLiteral((uint)frameHeader.LoopFilterParameters.FilterLevelV, 6);
}
}
writer.WriteLiteral((uint)frameHeader.LoopFilterParameters.SharpnessLevel, 3);
writer.WriteBoolean(frameHeader.LoopFilterParameters.ReferenceDeltaModeEnabled);
if (frameHeader.LoopFilterParameters.ReferenceDeltaModeEnabled)
{
writer.WriteBoolean(frameHeader.LoopFilterParameters.ReferenceDeltaModeUpdate);
if (frameHeader.LoopFilterParameters.ReferenceDeltaModeUpdate)
{
// An independent still frame can emit every current delta as an update. This is
// slightly larger than comparing against retained state but requires no video
// reference-frame state and produces the same observable filter parameters.
for (int i = 0; i < Av1Constants.TotalReferencesPerFrame; i++)
{
writer.WriteBoolean(true);
writer.WriteSignedFromUnsigned(frameHeader.LoopFilterParameters.ReferenceDeltas[i], 7);
}
for (int i = 0; i < 2; i++)
{
writer.WriteBoolean(true);
writer.WriteSignedFromUnsigned(frameHeader.LoopFilterParameters.ModeDeltas[i], 7);
}
}
}
}
/// <summary>
/// Writes the transform-size selection mode when the frame is not lossless.
/// </summary>
/// <param name="writer">The bit writer receiving the transform-mode flag.</param>
/// <param name="frameHeader">The frame header containing the transform mode.</param>
private static void WriteTransformMode(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader)
{
if (!frameHeader.CodedLossless)
{
writer.WriteBoolean(frameHeader.TransformMode == Av1TransformMode.Select);
}
}
/// <summary>
/// Writes the loop-restoration type and restoration-unit size for each plane.
/// </summary>
/// <param name="writer">The bit writer receiving the loop-restoration parameters.</param>
/// <param name="sequenceHeader">The sequence header defining restoration availability and color planes.</param>
/// <param name="frameHeader">The frame header containing restoration parameters.</param>
private static void WriteLoopRestorationParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader)
{
if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy || !sequenceHeader.EnableRestoration)
{
return;
}
int planesCount = sequenceHeader.ColorConfig.PlaneCount;
for (int i = 0; i < planesCount; i++)
{
writer.WriteLiteral((uint)frameHeader.LoopRestorationParameters.Items[i].Type, 2);
}
if (frameHeader.LoopRestorationParameters.UsesLoopRestoration)
{
uint unitShift = (uint)frameHeader.LoopRestorationParameters.UnitShift;
if (sequenceHeader.Use128x128Superblock)
{
writer.WriteLiteral(unitShift - 1, 1);
}
else
{
writer.WriteBoolean(unitShift > 0);
if (unitShift > 0)
{
writer.WriteLiteral(unitShift - 1, 1);
}
}
if (sequenceHeader.ColorConfig.SubSamplingX && sequenceHeader.ColorConfig.SubSamplingY && frameHeader.LoopRestorationParameters.UsesChromaLoopRestoration)
{
writer.WriteLiteral((uint)frameHeader.LoopRestorationParameters.UVShift, 1);
}
}
}
/// <summary>
/// Writes constrained directional enhancement filter strengths for the active planes.
/// </summary>
/// <param name="writer">The bit writer receiving the CDEF parameters.</param>
/// <param name="sequenceHeader">The sequence header defining CDEF availability and color planes.</param>
/// <param name="frameHeader">The frame header containing CDEF strengths.</param>
private static void WriteCdefParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader)
{
if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy || !sequenceHeader.EnableCdef)
{
return;
}
ObuConstraintDirectionalEnhancementFilterParameters cdef = frameHeader.CdefParameters;
writer.WriteLiteral((uint)cdef.Damping - 3, 2);
writer.WriteLiteral((uint)cdef.BitCount, 2);
int strengthCount = 1 << cdef.BitCount;
bool hasChroma = sequenceHeader.ColorConfig.PlaneCount > 1;
for (int i = 0; i < strengthCount; i++)
{
writer.WriteLiteral((uint)cdef.YStrength[i], 6);
if (hasChroma)
{
writer.WriteLiteral((uint)cdef.UvStrength[i], 6);
}
}
}
/// <summary>
/// Writes global-motion parameters when permitted by the frame type.
/// </summary>
/// <param name="writer">The bit writer positioned at the global-motion syntax.</param>
/// <param name="frameHeader">The current frame header.</param>
private static void WriteGlobalMotionParameters(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader)
{
_ = writer;
if (frameHeader.IsIntra)
{
// Nothing to be written for INTRA frames.
return;
}
throw new InvalidImageContentException("AVIF files can only contain INTRA frames.");
}
/// <summary>
/// Writes reference-mode selection when permitted by the frame type.
/// </summary>
/// <param name="writer">The bit writer positioned at the reference-mode syntax.</param>
/// <param name="frameHeader">The current frame header.</param>
private static void WriteFrameReferenceMode(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader)
{
_ = writer;
if (frameHeader.IsIntra)
{
// Nothing to be written for INTRA frames.
return;
}
throw new InvalidImageContentException("AVIF files can only contain INTRA frames.");
}
/// <summary>
/// Writes the skip-mode flag when skip mode is available.
/// </summary>
/// <param name="writer">The bit writer receiving the skip-mode flag.</param>
/// <param name="frameHeader">The frame header containing skip-mode state.</param>
private static void WriteSkipModeParameters(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader)
{
if (frameHeader.SkipModeParameters.SkipModeAllowed)
{
writer.WriteBoolean(frameHeader.SkipModeParameters.SkipModeFlag);
}
}
/// <summary>
/// Writes film-grain synthesis parameters for a displayed still-image frame.
/// </summary>
/// <param name="writer">The bit writer receiving the film-grain parameters.</param>
/// <param name="sequenceHeader">The sequence header defining film-grain availability and color sampling.</param>
/// <param name="frameHeader">The frame header containing film-grain parameters.</param>
private static void WriteFilmGrainFilterParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader)
{
ObuFilmGrainParameters grainParams = frameHeader.FilmGrainParameters;
if (!sequenceHeader.AreFilmGrainingParametersPresent || (!frameHeader.ShowFrame && !frameHeader.ShowableFrame))
{
return;
}
writer.WriteBoolean(grainParams.ApplyGrain);
if (!grainParams.ApplyGrain)
{
return;
}
writer.WriteLiteral(grainParams.GrainSeed, 16);
writer.WriteLiteral(grainParams.NumYPoints, 4);
for (int i = 0; i < grainParams.NumYPoints; i++)
{
writer.WriteLiteral(grainParams.PointYValue[i], 8);
writer.WriteLiteral(grainParams.PointYScaling[i], 8);
}
if (!sequenceHeader.ColorConfig.IsMonochrome)
{
writer.WriteBoolean(grainParams.ChromaScalingFromLuma);
}
if (!sequenceHeader.ColorConfig.IsMonochrome &&
!grainParams.ChromaScalingFromLuma &&
(!sequenceHeader.ColorConfig.SubSamplingX || !sequenceHeader.ColorConfig.SubSamplingY || grainParams.NumYPoints != 0))
{
writer.WriteLiteral(grainParams.NumCbPoints, 4);
for (int i = 0; i < grainParams.NumCbPoints; i++)
{
writer.WriteLiteral(grainParams.PointCbValue[i], 8);
writer.WriteLiteral(grainParams.PointCbScaling[i], 8);
}
writer.WriteLiteral(grainParams.NumCrPoints, 4);
for (int i = 0; i < grainParams.NumCrPoints; i++)
{
writer.WriteLiteral(grainParams.PointCrValue[i], 8);
writer.WriteLiteral(grainParams.PointCrScaling[i], 8);
}
}
writer.WriteLiteral(grainParams.GrainScalingMinus8, 2);
writer.WriteLiteral(grainParams.ArCoeffLag, 2);
uint numPosLuma = 2 * grainParams.ArCoeffLag * (grainParams.ArCoeffLag + 1);
uint numPosChroma = numPosLuma;
if (grainParams.NumYPoints != 0)
{
numPosChroma++;
for (int i = 0; i < numPosLuma; i++)
{
writer.WriteLiteral(grainParams.ArCoeffsYPlus128[i], 8);
}
}
if (grainParams.ChromaScalingFromLuma || grainParams.NumCbPoints != 0)
{
for (int i = 0; i < numPosChroma; i++)
{
writer.WriteLiteral(grainParams.ArCoeffsCbPlus128[i], 8);
}
}
if (grainParams.ChromaScalingFromLuma || grainParams.NumCrPoints != 0)
{
for (int i = 0; i < numPosChroma; i++)
{
writer.WriteLiteral(grainParams.ArCoeffsCrPlus128[i], 8);
}
}
writer.WriteLiteral(grainParams.ArCoeffShiftMinus6, 2);
writer.WriteLiteral(grainParams.GrainScaleShift, 2);
if (grainParams.NumCbPoints != 0)
{
writer.WriteLiteral(grainParams.CbMult, 8);
writer.WriteLiteral(grainParams.CbLumaMult, 8);
writer.WriteLiteral(grainParams.CbOffset, 9);
}
if (grainParams.NumCrPoints != 0)
{
writer.WriteLiteral(grainParams.CrMult, 8);
writer.WriteLiteral(grainParams.CrLumaMult, 8);
writer.WriteLiteral(grainParams.CrOffset, 9);
}
writer.WriteBoolean(grainParams.OverlapFlag);
writer.WriteBoolean(grainParams.ClipToRestrictedRange);
}
}

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

@ -0,0 +1,205 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.SuperResolution;
using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
/// <summary>
/// Reconstructs the coded blocks of one AV1 image frame into planar sample buffers.
/// </summary>
internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable
{
/// <summary>
/// The sequence-level superblock and color configuration.
/// </summary>
private readonly ObuSequenceHeader sequenceHeader;
/// <summary>
/// The frame-level tile, quantization, and reconstruction configuration.
/// </summary>
private readonly ObuFrameHeader frameHeader;
/// <summary>
/// The parsed superblock and block-mode information for the frame.
/// </summary>
private readonly Av1FrameInfo frameInfo;
/// <summary>
/// The destination planar sample buffers for reconstructed pixels.
/// </summary>
private readonly Av1FrameBuffer<byte> frameBuffer;
/// <summary>
/// The retained reconstructed frames addressable by inter prediction.
/// </summary>
private readonly Av1ReferenceFrameStore referenceFrames;
/// <summary>
/// The coefficient inverse-quantization stage shared across superblocks.
/// </summary>
private readonly Av1InverseQuantizer inverseQuantizer;
/// <summary>
/// The frame's base per-segment and per-plane dequantization values.
/// </summary>
private readonly Av1DeQuantizationContext deQuants;
/// <summary>
/// The transform-size map populated during reconstruction and consumed by deblocking.
/// </summary>
private readonly Av1LoopFilterContext loopFilterContext;
/// <summary>
/// The block reconstruction stage that applies prediction and inverse transforms.
/// </summary>
private readonly Av1BlockDecoder blockDecoder;
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameDecoder"/> class.
/// </summary>
/// <param name="sequenceHeader">The parsed AV1 sequence header.</param>
/// <param name="frameHeader">The parsed AV1 frame header.</param>
/// <param name="frameInfo">The parsed superblock and block-mode information.</param>
/// <param name="frameBuffer">The destination planar sample buffers.</param>
/// <param name="referenceFrames">The retained reconstructed frames selected by inter blocks.</param>
/// <param name="paletteColorIndexMaps">The complete decoder-session palette map state.</param>
public Av1FrameDecoder(
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader,
Av1FrameInfo frameInfo,
Av1FrameBuffer<byte> frameBuffer,
Av1ReferenceFrameStore referenceFrames,
Av1TileReader.PaletteColorIndexMaps? paletteColorIndexMaps = null)
{
this.sequenceHeader = sequenceHeader;
this.frameHeader = frameHeader;
this.frameInfo = frameInfo;
this.frameBuffer = frameBuffer;
this.referenceFrames = referenceFrames;
this.inverseQuantizer = new(sequenceHeader, frameHeader);
this.deQuants = new(sequenceHeader, frameHeader);
this.loopFilterContext = new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
try
{
this.blockDecoder = new(
this.sequenceHeader,
this.frameHeader,
this.frameBuffer,
this.loopFilterContext,
this.inverseQuantizer,
this.referenceFrames,
paletteColorIndexMaps);
}
catch
{
this.loopFilterContext.Dispose();
throw;
}
}
/// <summary>
/// Releases the pooled block-reconstruction workspaces owned by this decoder.
/// </summary>
public void Dispose()
{
this.blockDecoder.Dispose();
this.loopFilterContext.Dispose();
}
/// <summary>
/// Applies the in-loop frame stages after every superblock has been reconstructed.
/// </summary>
public void CompleteFrame()
{
bool doLoopRestoration = this.frameHeader.LoopRestorationParameters.UsesLoopRestoration;
Av1LoopFilterDecoder loopFilterDecoder = new(
this.sequenceHeader,
this.frameHeader,
this.frameInfo,
this.frameBuffer,
this.loopFilterContext);
loopFilterDecoder.DecodeFrame();
using Av1LoopRestorationBoundary? restorationBoundary = doLoopRestoration
? new(this.sequenceHeader, this.frameHeader, this.frameBuffer)
: null;
if (restorationBoundary is not null)
{
restorationBoundary.SaveDeblockedRows();
}
Av1CdefDecoder cdefDecoder = new(this.sequenceHeader, this.frameHeader, this.frameInfo, this.frameBuffer);
cdefDecoder.DecodeFrame();
Av1SuperResolutionDecoder superResolutionDecoder = new(this.sequenceHeader, this.frameHeader, this.frameBuffer);
superResolutionDecoder.DecodeFrame();
if (restorationBoundary is not null)
{
restorationBoundary.SaveFrameEdgeRows();
Av1LoopRestorationDecoder loopRestorationDecoder = new(
this.sequenceHeader,
this.frameHeader,
this.frameInfo,
this.frameBuffer,
restorationBoundary);
loopRestorationDecoder.DecodeFrame();
}
// Film grain is deliberately excluded here because this buffer is the normative post-restoration reference.
// The owning decoder applies grain only to the presentation buffer after reference ownership is established.
}
/// <summary>
/// Reconstructs one superblock after applying its block state and delta-Q context.
/// </summary>
/// <param name="modeInfoPosition">The superblock's top-left position in 4x4 mode-info units.</param>
/// <param name="superblockInfo">The decoded syntax and block modes for the superblock.</param>
/// <param name="tileInfo">The tile that contains the superblock.</param>
public void DecodeSuperblock(Point modeInfoPosition, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo)
{
this.blockDecoder.UpdateSuperblock(superblockInfo);
this.inverseQuantizer.UpdateDequant(this.deQuants, superblockInfo);
this.DecodePartition(modeInfoPosition, superblockInfo, tileInfo);
}
/// <summary>
/// Reconstructs each decoded block in a superblock partition.
/// </summary>
/// <param name="modeInfoPosition">The superblock's frame-relative origin in 4x4 mode-info units.</param>
/// <param name="superblockInfo">The superblock whose block modes are traversed.</param>
/// <param name="tileInfo">The tile boundary information used by intra prediction.</param>
/// <remarks>Traverses the depth-first block order produced by tile parsing.</remarks>
private void DecodePartition(Point modeInfoPosition, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo)
{
foreach (ref Av1BlockModeInfo modeInfo in superblockInfo.GetModeInfos())
{
Point subPosition = modeInfo.PositionInSuperblock;
Av1BlockSize subSize = modeInfo.BlockSize;
Point globalPosition = new(modeInfoPosition.X, modeInfoPosition.Y);
// Block positions are stored relative to the superblock; prediction and reconstruction require frame-relative mode-info coordinates.
globalPosition.Offset(subPosition);
this.blockDecoder.DecodeBlock(modeInfo, globalPosition, subSize, superblockInfo, tileInfo);
// Palette maps are decoder-session scratch. Retained mode information must not keep views after the block
// has consumed them because the next superblock reuses the same storage.
modeInfo.SetPaletteColorIndexMap(Av1PlaneType.Y, default);
modeInfo.SetPaletteColorIndexMap(Av1PlaneType.Uv, default);
}
}
}

35
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs

@ -0,0 +1,35 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
/// <summary>
/// Defines the work-in-progress AV1 still-image frame-encoding boundary.
/// </summary>
internal class Av1FrameEncoder
{
/// <summary>
/// The source plane samples supplied for frame encoding.
/// </summary>
private readonly Av1FrameBuffer<byte> frameBuffer;
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameEncoder"/> class.
/// </summary>
/// <param name="frameBuffer">The source frame samples to encode.</param>
public Av1FrameEncoder(Av1FrameBuffer<byte> frameBuffer)
{
this.frameBuffer = frameBuffer;
}
/// <summary>
/// Represents the not-yet-implemented entry point for encoding one AV1 still-image frame.
/// </summary>
public static void Encode()
{
// Still-image encoding needs the normative analysis, transform, quantization, entropy, and packetization stages,
// but it does not require the encoder's application-level worker graph or video-sequence process orchestration.
}
}

731
src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs

@ -0,0 +1,731 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
/// <summary>
/// Applies AV1 constrained directional enhancement filtering to a reconstructed still-image frame.
/// </summary>
internal sealed class Av1CdefDecoder
{
/// <summary>
/// The width and height of a CDEF unit in 4x4 luma mode-information units.
/// </summary>
private const int CdefUnitModeInfoSize = 16;
/// <summary>
/// The number of unavailable samples reserved on each source-plane edge.
/// </summary>
private const int SourceBorder = 2;
/// <summary>
/// The maximum number of non-skipped 8x8 luma blocks in one 64x64 CDEF unit.
/// </summary>
private const int MaximumBlocksPerUnit = 8 * 8;
/// <summary>
/// The maximum width or height of one CDEF unit in plane samples.
/// </summary>
private const int MaximumUnitPlaneSize = CdefUnitModeInfoSize << Av1Constants.ModeInfoSizeLog2;
/// <summary>
/// The stride of the reusable bordered CDEF source unit.
/// </summary>
private const int SourceStride = MaximumUnitPlaneSize + (SourceBorder * 2);
/// <summary>
/// The sample count of the reusable bordered CDEF source unit.
/// </summary>
private const int SourceBufferLength = SourceStride * (MaximumUnitPlaneSize + (SourceBorder * 2));
/// <summary>
/// The sequence-level superblock, bit-depth, and color configuration.
/// </summary>
private readonly ObuSequenceHeader sequenceHeader;
/// <summary>
/// The frame dimensions and CDEF strength table.
/// </summary>
private readonly ObuFrameHeader frameHeader;
/// <summary>
/// The decoded block skip state and CDEF-unit strength selections.
/// </summary>
private readonly Av1FrameInfo frameInfo;
/// <summary>
/// The reconstructed plane samples modified by CDEF.
/// </summary>
private readonly Av1FrameBuffer<byte> frameBuffer;
/// <summary>
/// Initializes a new instance of the <see cref="Av1CdefDecoder"/> class.
/// </summary>
/// <param name="sequenceHeader">The sequence header defining CDEF availability and the color layout.</param>
/// <param name="frameHeader">The frame header defining dimensions and CDEF strengths.</param>
/// <param name="frameInfo">The decoded block skip state and per-unit strength selections.</param>
/// <param name="frameBuffer">The deblocked frame samples to filter.</param>
public Av1CdefDecoder(
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader,
Av1FrameInfo frameInfo,
Av1FrameBuffer<byte> frameBuffer)
{
this.sequenceHeader = sequenceHeader;
this.frameHeader = frameHeader;
this.frameInfo = frameInfo;
this.frameBuffer = frameBuffer;
}
/// <summary>
/// Filters every enabled color plane using directions derived from the deblocked luma plane.
/// </summary>
public void DecodeFrame()
{
if (!this.sequenceHeader.EnableCdef || this.frameHeader.CodedLossless || this.frameHeader.AllowIntraBlockCopy)
{
return;
}
ObuConstraintDirectionalEnhancementFilterParameters parameters = this.frameHeader.CdefParameters;
int strengthCount = 1 << parameters.BitCount;
bool hasNonZeroStrength = false;
for (int i = 0; i < strengthCount; i++)
{
if (parameters.YStrength[i] != 0 ||
(this.sequenceHeader.ColorConfig.PlaneCount > 1 && parameters.UvStrength[i] != 0))
{
hasNonZeroStrength = true;
break;
}
}
if (!hasNonZeroStrength)
{
return;
}
ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig;
int planeCount = colorConfig.PlaneCount;
Span<int> subsamplingX = stackalloc int[3];
Span<int> subsamplingY = stackalloc int[3];
Span<int> planeWidths = stackalloc int[3];
Span<int> lineBufferOffsets = stackalloc int[3];
Span<int> columnBufferOffsets = stackalloc int[3];
Span<int> columnBufferLengths = stackalloc int[3];
int lineBufferLength = 0;
int columnBufferLength = 0;
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++)
{
Av1Plane plane = (Av1Plane)planeIndex;
int planeSubsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0;
int planeSubsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0;
int planeWidth = this.frameHeader.ModeInfoColumnCount << (Av1Constants.ModeInfoSizeLog2 - planeSubsamplingX);
int maximumUnitHeight = MaximumUnitPlaneSize >> planeSubsamplingY;
subsamplingX[planeIndex] = planeSubsamplingX;
subsamplingY[planeIndex] = planeSubsamplingY;
planeWidths[planeIndex] = planeWidth;
lineBufferOffsets[planeIndex] = lineBufferLength;
columnBufferOffsets[planeIndex] = columnBufferLength;
columnBufferLengths[planeIndex] = (maximumUnitHeight + (SourceBorder * 2)) * SourceBorder;
lineBufferLength += planeWidth * SourceBorder * 2;
columnBufferLength += columnBufferLengths[planeIndex];
}
int scratchLength = SourceBufferLength + lineBufferLength + columnBufferLength;
MemoryAllocator allocator = this.frameBuffer.MemoryAllocator;
using IMemoryOwner<ushort> scratchOwner = allocator.Allocate<ushort>(scratchLength);
Span<ushort> scratch = scratchOwner.Memory.Span[..scratchLength];
Span<ushort> source = scratch[..SourceBufferLength];
Span<ushort> lineBuffer = scratch.Slice(SourceBufferLength, lineBufferLength);
Span<ushort> columnBuffer = scratch[(SourceBufferLength + lineBufferLength)..];
Span<int> directions = stackalloc int[MaximumBlocksPerUnit];
Span<int> variances = stackalloc int[MaximumBlocksPerUnit];
Span<bool> cdefLeft = stackalloc bool[3];
int unitColumnCount = (this.frameHeader.ModeInfoColumnCount + CdefUnitModeInfoSize - 1) / CdefUnitModeInfoSize;
int unitRowCount = (this.frameHeader.ModeInfoRowCount + CdefUnitModeInfoSize - 1) / CdefUnitModeInfoSize;
// libaom traverses one 64x64 unit at a time so chroma consumes the luma directions before
// the fixed direction arrays are reused. This also bounds direction storage to 64 entries.
for (int unitRow = 0; unitRow < unitRowCount; unitRow++)
{
cdefLeft.Clear();
int unitModeInfoRow = unitRow * CdefUnitModeInfoSize;
// Preserve the final two unfiltered rows before this unit row is modified. The alternating
// slots keep the previous row available while the next row's top border is captured.
if (unitRow < unitRowCount - 1)
{
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++)
{
Av1Plane plane = (Av1Plane)planeIndex;
int planeSubsamplingX = subsamplingX[planeIndex];
int planeSubsamplingY = subsamplingY[planeIndex];
int planeWidth = planeWidths[planeIndex];
int nextPlaneRow = ((unitModeInfoRow + CdefUnitModeInfoSize) << Av1Constants.ModeInfoSizeLog2) >> planeSubsamplingY;
int lineSlotOffset = lineBufferOffsets[planeIndex] +
((unitRow & 1) * SourceBorder * planeWidth);
this.GetPlaneDestination(
plane,
planeSubsamplingX,
planeSubsamplingY,
out Span<byte> lowBitDepthDestination,
out Span<ushort> highBitDepthDestination,
out int destinationStride);
this.CopyFrameRectangle(
lowBitDepthDestination,
highBitDepthDestination,
destinationStride + ((nextPlaneRow - SourceBorder) * destinationStride),
destinationStride,
lineBuffer,
lineSlotOffset,
planeWidth,
planeWidth,
SourceBorder);
}
}
for (int unitColumn = 0; unitColumn < unitColumnCount; unitColumn++)
{
int unitModeInfoColumn = unitColumn * CdefUnitModeInfoSize;
int strengthIndex = this.GetStrengthIndex(unitModeInfoColumn, unitModeInfoRow);
if (strengthIndex < 0)
{
cdefLeft.Clear();
continue;
}
int yStrength = parameters.YStrength[strengthIndex];
int uvStrength = parameters.UvStrength[strengthIndex];
bool unitNeedsDirections = yStrength != 0 || (planeCount > 1 && uvStrength != 0);
if (!unitNeedsDirections)
{
cdefLeft.Clear();
continue;
}
int unitModeInfoRowEnd = Math.Min(unitModeInfoRow + CdefUnitModeInfoSize, this.frameHeader.ModeInfoRowCount);
int unitModeInfoColumnEnd = Math.Min(unitModeInfoColumn + CdefUnitModeInfoSize, this.frameHeader.ModeInfoColumnCount);
CdefBlockList blocks = default;
int blockCount = 0;
for (int blockModeInfoRow = unitModeInfoRow; blockModeInfoRow < unitModeInfoRowEnd; blockModeInfoRow += 2)
{
for (int blockModeInfoColumn = unitModeInfoColumn; blockModeInfoColumn < unitModeInfoColumnEnd; blockModeInfoColumn += 2)
{
if (this.IsBlockSkipped(blockModeInfoColumn, blockModeInfoRow))
{
continue;
}
blocks[blockCount++] = new CdefBlock(blockModeInfoColumn, blockModeInfoRow);
}
}
if (blockCount == 0)
{
cdefLeft.Clear();
continue;
}
// Luma is always prepared first when either plane type needs CDEF because it owns
// the direction search. Chroma then reuses those per-unit results without a frame map.
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++)
{
if (planeIndex != (int)Av1Plane.Y && uvStrength == 0)
{
cdefLeft[planeIndex] = false;
continue;
}
int planeWidth = planeWidths[planeIndex];
int currentLineSlotOffset = lineBufferOffsets[planeIndex] +
(((unitRow - 1) & 1) * SourceBorder * planeWidth);
ReadOnlySpan<ushort> topLineBuffer = unitRow == 0
? default
: lineBuffer.Slice(currentLineSlotOffset, SourceBorder * planeWidth);
Span<ushort> planeColumnBuffer = columnBuffer.Slice(
columnBufferOffsets[planeIndex],
columnBufferLengths[planeIndex]);
this.FilterPlane(
(Av1Plane)planeIndex,
subsamplingX[planeIndex],
subsamplingY[planeIndex],
unitModeInfoColumn,
unitModeInfoRow,
ref blocks,
blockCount,
directions,
variances,
yStrength,
uvStrength,
source,
topLineBuffer,
planeColumnBuffer,
cdefLeft[planeIndex]);
cdefLeft[planeIndex] = true;
}
}
}
}
/// <summary>
/// Filters one color plane in a CDEF unit from a bounded immutable source snapshot.
/// </summary>
/// <param name="plane">The color plane to filter.</param>
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
/// <param name="subsamplingY">The vertical chroma subsampling shift.</param>
/// <param name="unitModeInfoColumn">The unit's frame-relative column in 4x4 luma units.</param>
/// <param name="unitModeInfoRow">The unit's frame-relative row in 4x4 luma units.</param>
/// <param name="blocks">The unit's non-skipped 8x8 luma blocks.</param>
/// <param name="blockCount">The number of initialized entries in <paramref name="blocks"/>.</param>
/// <param name="directions">The unit-local luma directions in block-list order.</param>
/// <param name="variances">The unit-local luma directional variances in block-list order.</param>
/// <param name="yStrength">The coded luma strength.</param>
/// <param name="uvStrength">The coded chroma strength.</param>
/// <param name="source">The reusable bordered source-unit buffer.</param>
/// <param name="topLineBuffer">The two preserved unfiltered rows immediately above this unit row.</param>
/// <param name="columnBuffer">The preserved unfiltered columns immediately left of this unit.</param>
/// <param name="leftPrepared">Whether the preceding unit overwrote samples needed by this unit.</param>
private void FilterPlane(
Av1Plane plane,
int subsamplingX,
int subsamplingY,
int unitModeInfoColumn,
int unitModeInfoRow,
ref CdefBlockList blocks,
int blockCount,
Span<int> directions,
Span<int> variances,
int yStrength,
int uvStrength,
Span<ushort> source,
ReadOnlySpan<ushort> topLineBuffer,
Span<ushort> columnBuffer,
bool leftPrepared)
{
int planeWidth = this.frameHeader.ModeInfoColumnCount << (Av1Constants.ModeInfoSizeLog2 - subsamplingX);
int planeHeight = this.frameHeader.ModeInfoRowCount << (Av1Constants.ModeInfoSizeLog2 - subsamplingY);
int planeColumn = (unitModeInfoColumn << Av1Constants.ModeInfoSizeLog2) >> subsamplingX;
int planeRow = (unitModeInfoRow << Av1Constants.ModeInfoSizeLog2) >> subsamplingY;
int unitWidth = Math.Min(MaximumUnitPlaneSize >> subsamplingX, planeWidth - planeColumn);
int unitHeight = Math.Min(MaximumUnitPlaneSize >> subsamplingY, planeHeight - planeRow);
bool hasLeft = planeColumn > 0;
bool hasRight = planeColumn + unitWidth < planeWidth;
bool hasTop = planeRow > 0;
bool hasBottom = planeRow + unitHeight < planeHeight;
int leftSampleCount = hasLeft ? SourceBorder : 0;
int rightSampleCount = hasRight ? SourceBorder : 0;
int copyColumn = planeColumn - leftSampleCount;
int copyWidth = leftSampleCount + unitWidth + rightSampleCount;
int sourceColumn = SourceBorder - leftSampleCount;
// CDEF output must never become input to a later unit. libaom therefore reconstructs a
// bordered unit from saved top/left samples and still-unmodified frame samples. Filling first
// also gives every unavailable frame-edge tap the normative CDEF_VERY_LARGE sentinel.
source.Fill(Av1CdefFilter.VeryLarge);
this.GetPlaneDestination(
plane,
subsamplingX,
subsamplingY,
out Span<byte> lowBitDepthDestination,
out Span<ushort> highBitDepthDestination,
out int destinationStride);
if (hasTop)
{
Av1CdefFilter.CopyPlane(
topLineBuffer,
copyColumn,
planeWidth,
source,
sourceColumn,
SourceStride,
copyWidth,
SourceBorder);
}
this.CopyFrameRectangle(
lowBitDepthDestination,
highBitDepthDestination,
destinationStride + (planeRow * destinationStride) + copyColumn,
destinationStride,
source,
(SourceBorder * SourceStride) + sourceColumn,
SourceStride,
copyWidth,
unitHeight);
if (hasBottom)
{
this.CopyFrameRectangle(
lowBitDepthDestination,
highBitDepthDestination,
destinationStride + ((planeRow + unitHeight) * destinationStride) + copyColumn,
destinationStride,
source,
((SourceBorder + unitHeight) * SourceStride) + sourceColumn,
SourceStride,
copyWidth,
SourceBorder);
}
int preservedHeight = SourceBorder + unitHeight + (hasBottom ? SourceBorder : 0);
if (leftPrepared)
{
Av1CdefFilter.CopyPlane(
columnBuffer,
0,
SourceBorder,
source,
0,
SourceStride,
SourceBorder,
preservedHeight);
}
// Save the final unfiltered columns before this unit writes its destination. The next unit
// restores them over the frame samples that this unit has already replaced.
Av1CdefFilter.CopyPlane(
source,
unitWidth,
SourceStride,
columnBuffer,
0,
SourceBorder,
SourceBorder,
preservedHeight);
ObuConstraintDirectionalEnhancementFilterParameters parameters = this.frameHeader.CdefParameters;
int coefficientShift = Math.Max(this.frameBuffer.BitDepth.GetBitCount() - 8, 0);
int blockWidth = 8 >> subsamplingX;
int blockHeight = 8 >> subsamplingY;
int codedStrength = plane == Av1Plane.Y ? yStrength : uvStrength;
int primaryStrength = (codedStrength / 4) << coefficientShift;
int secondaryStrength = codedStrength % 4;
// The two-bit secondary field leaves value three unused and represents strength four instead.
secondaryStrength += secondaryStrength == 3 ? 1 : 0;
secondaryStrength <<= coefficientShift;
int damping = parameters.Damping + coefficientShift - (plane == Av1Plane.Y ? 0 : 1);
if (plane == Av1Plane.Y)
{
int blockIndex = 0;
// The reference decoder analyzes two listed 8x8 blocks together. The per-unit fixed list preserves that traversal
// without allocating a managed block list or repeating four skip-map lookups during filtering.
for (; blockIndex < blockCount - 1; blockIndex += 2)
{
CdefBlock firstBlock = blocks[blockIndex];
CdefBlock secondBlock = blocks[blockIndex + 1];
Av1CdefFilter.FindDirections(
source,
firstBlock.GetSourceOffset(SourceStride, SourceBorder, unitModeInfoColumn, unitModeInfoRow, 0, 0),
secondBlock.GetSourceOffset(SourceStride, SourceBorder, unitModeInfoColumn, unitModeInfoRow, 0, 0),
SourceStride,
coefficientShift,
out directions[blockIndex],
out variances[blockIndex],
out directions[blockIndex + 1],
out variances[blockIndex + 1]);
}
if (blockIndex < blockCount)
{
CdefBlock block = blocks[blockIndex];
directions[blockIndex] = Av1CdefFilter.FindDirection(
source,
block.GetSourceOffset(SourceStride, SourceBorder, unitModeInfoColumn, unitModeInfoRow, 0, 0),
SourceStride,
coefficientShift,
out variances[blockIndex]);
}
}
if (codedStrength == 0)
{
return;
}
for (int blockIndex = 0; blockIndex < blockCount; blockIndex++)
{
CdefBlock block = blocks[blockIndex];
int filteredPrimaryStrength = plane == Av1Plane.Y
? Av1CdefFilter.AdjustStrength(primaryStrength, variances[blockIndex])
: primaryStrength;
if (filteredPrimaryStrength == 0 && secondaryStrength == 0)
{
continue;
}
// Secondary-only filtering uses direction zero; otherwise chroma remaps the
// luma direction into its asymmetrically subsampled sample grid when required.
int direction = primaryStrength != 0
? Av1CdefFilter.ConvertDirection(directions[blockIndex], subsamplingX, subsamplingY)
: 0;
int blockPlaneColumn = (block.ModeInfoColumn << Av1Constants.ModeInfoSizeLog2) >> subsamplingX;
int blockPlaneRow = (block.ModeInfoRow << Av1Constants.ModeInfoSizeLog2) >> subsamplingY;
int blockSourceOffset = block.GetSourceOffset(
SourceStride,
SourceBorder,
unitModeInfoColumn,
unitModeInfoRow,
subsamplingX,
subsamplingY);
int blockDestinationOffset = destinationStride + (blockPlaneRow * destinationStride) + blockPlaneColumn;
if (this.frameBuffer.BytesPerSample == 2)
{
Av1CdefFilter.FilterBlock(
source,
blockSourceOffset,
SourceStride,
highBitDepthDestination,
blockDestinationOffset,
destinationStride,
filteredPrimaryStrength,
secondaryStrength,
direction,
damping,
damping,
coefficientShift,
blockWidth,
blockHeight);
}
else
{
Av1CdefFilter.FilterBlock(
source,
blockSourceOffset,
SourceStride,
lowBitDepthDestination,
blockDestinationOffset,
destinationStride,
filteredPrimaryStrength,
secondaryStrength,
direction,
damping,
damping,
coefficientShift,
blockWidth,
blockHeight);
}
}
}
/// <summary>
/// Gets the byte or native 16-bit destination span for one frame plane.
/// </summary>
/// <param name="plane">The color plane.</param>
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
/// <param name="subsamplingY">The vertical chroma subsampling shift.</param>
/// <param name="lowBitDepthDestination">Receives the byte destination for an eight-bit frame.</param>
/// <param name="highBitDepthDestination">Receives the native destination for a high-bit-depth frame.</param>
/// <param name="destinationStride">Receives the number of samples between adjacent rows.</param>
private void GetPlaneDestination(
Av1Plane plane,
int subsamplingX,
int subsamplingY,
out Span<byte> lowBitDepthDestination,
out Span<ushort> highBitDepthDestination,
out int destinationStride)
{
lowBitDepthDestination = default;
highBitDepthDestination = default;
if (this.frameBuffer.BytesPerSample == 2)
{
Span<short> signedDestination = this.frameBuffer.DeriveBlockPointer16(
plane,
Point.Empty,
subsamplingX,
subsamplingY,
out destinationStride);
highBitDepthDestination = MemoryMarshal.Cast<short, ushort>(signedDestination);
}
else
{
lowBitDepthDestination = this.frameBuffer.DeriveBlockPointer(
plane,
Point.Empty,
subsamplingX,
subsamplingY,
out destinationStride);
}
}
/// <summary>
/// Copies one frame rectangle into 16-bit CDEF working storage.
/// </summary>
/// <param name="lowBitDepthSource">The byte source for an eight-bit frame.</param>
/// <param name="highBitDepthSource">The native source for a high-bit-depth frame.</param>
/// <param name="sourceOffset">The offset of the rectangle's top-left source sample.</param>
/// <param name="sourceStride">The number of samples between adjacent source rows.</param>
/// <param name="destination">The 16-bit working destination.</param>
/// <param name="destinationOffset">The offset of the rectangle's top-left destination sample.</param>
/// <param name="destinationStride">The number of samples between adjacent destination rows.</param>
/// <param name="width">The rectangle width in samples.</param>
/// <param name="height">The rectangle height in samples.</param>
private void CopyFrameRectangle(
ReadOnlySpan<byte> lowBitDepthSource,
ReadOnlySpan<ushort> highBitDepthSource,
int sourceOffset,
int sourceStride,
Span<ushort> destination,
int destinationOffset,
int destinationStride,
int width,
int height)
{
if (this.frameBuffer.BytesPerSample == 2)
{
Av1CdefFilter.CopyPlane(
highBitDepthSource,
sourceOffset,
sourceStride,
destination,
destinationOffset,
destinationStride,
width,
height);
}
else
{
Av1CdefFilter.CopyPlane(
lowBitDepthSource,
sourceOffset,
sourceStride,
destination,
destinationOffset,
destinationStride,
width,
height);
}
}
/// <summary>
/// Gets the strength-table selection assigned to a 64x64 CDEF unit.
/// </summary>
/// <param name="modeInfoColumn">The unit's frame-relative column in 4x4 luma units.</param>
/// <param name="modeInfoRow">The unit's frame-relative row in 4x4 luma units.</param>
/// <returns>The strength-table index, or minus one when every block in the unit is skipped.</returns>
private int GetStrengthIndex(int modeInfoColumn, int modeInfoRow)
{
int superblockModeInfoSize = this.frameInfo.SuperblockModeInfoSize;
Point superblockPosition = new(
modeInfoColumn / superblockModeInfoSize,
modeInfoRow / superblockModeInfoSize);
int unitColumn = (modeInfoColumn % superblockModeInfoSize) / CdefUnitModeInfoSize;
int unitRow = (modeInfoRow % superblockModeInfoSize) / CdefUnitModeInfoSize;
// A 128x128 superblock stores four raster-ordered 64x64 selections; the same
// expression naturally resolves to index zero for a 64x64 superblock.
int unitIndex = unitColumn + (unitRow << 1);
return this.frameInfo.GetCdefStrength(superblockPosition)[unitIndex];
}
/// <summary>
/// Determines whether every 4x4 mode-information block covered by an 8x8 CDEF block is skipped.
/// </summary>
/// <param name="modeInfoColumn">The block's frame-relative column in 4x4 luma units.</param>
/// <param name="modeInfoRow">The block's frame-relative row in 4x4 luma units.</param>
/// <returns><see langword="true"/> when the complete 8x8 block is skipped; otherwise, <see langword="false"/>.</returns>
private bool IsBlockSkipped(int modeInfoColumn, int modeInfoRow)
{
for (int row = 0; row < 2; row++)
{
for (int column = 0; column < 2; column++)
{
if (!this.frameInfo.GetModeInfoAt(new Point(modeInfoColumn + column, modeInfoRow + row)).Skip)
{
return false;
}
}
}
return true;
}
/// <summary>
/// Stores the non-skipped blocks in one CDEF unit without a managed allocation.
/// </summary>
[InlineArray(MaximumBlocksPerUnit)]
private struct CdefBlockList
{
/// <summary>
/// The first block in the inline storage.
/// </summary>
private CdefBlock element0;
}
/// <summary>
/// Identifies one 8x8 luma block by its frame-relative mode-information coordinates.
/// </summary>
private readonly struct CdefBlock
{
/// <summary>
/// Initializes a new instance of the <see cref="CdefBlock"/> struct.
/// </summary>
/// <param name="modeInfoColumn">The frame-relative column in 4x4 luma units.</param>
/// <param name="modeInfoRow">The frame-relative row in 4x4 luma units.</param>
public CdefBlock(int modeInfoColumn, int modeInfoRow)
{
this.ModeInfoColumn = modeInfoColumn;
this.ModeInfoRow = modeInfoRow;
}
/// <summary>
/// Gets the frame-relative column in 4x4 luma units.
/// </summary>
public int ModeInfoColumn { get; }
/// <summary>
/// Gets the frame-relative row in 4x4 luma units.
/// </summary>
public int ModeInfoRow { get; }
/// <summary>
/// Gets the block offset in a bordered CDEF source unit.
/// </summary>
/// <param name="sourceStride">The number of samples between adjacent source rows.</param>
/// <param name="sourceBorder">The number of unavailable samples surrounding the source.</param>
/// <param name="unitModeInfoColumn">The unit's frame-relative column in 4x4 luma units.</param>
/// <param name="unitModeInfoRow">The unit's frame-relative row in 4x4 luma units.</param>
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
/// <param name="subsamplingY">The vertical chroma subsampling shift.</param>
/// <returns>The source-unit offset.</returns>
public int GetSourceOffset(
int sourceStride,
int sourceBorder,
int unitModeInfoColumn,
int unitModeInfoRow,
int subsamplingX,
int subsamplingY)
{
int planeColumn = ((this.ModeInfoColumn - unitModeInfoColumn) << Av1Constants.ModeInfoSizeLog2) >> subsamplingX;
int planeRow = ((this.ModeInfoRow - unitModeInfoRow) << Av1Constants.ModeInfoSizeLog2) >> subsamplingY;
return ((planeRow + sourceBorder) * sourceStride) + planeColumn + sourceBorder;
}
}
}

34
src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.ByteOutputOperator.cs

@ -0,0 +1,34 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
internal static partial class Av1CdefFilter
{
/// <summary>
/// Writes filtered samples to eight-bit plane storage.
/// </summary>
private readonly struct ByteOutputOperator : IOutputOperator<byte>
{
/// <inheritdoc/>
public static void StoreVector(ref byte destination, int offset, Vector128<short> value, int count)
{
Vector64<byte> packed = Vector128.Narrow(value.AsUInt16(), Vector128<ushort>.Zero).GetLower();
ref byte output = ref Unsafe.Add(ref destination, offset);
if (count == 8)
{
packed.StoreUnsafe(ref output);
}
else
{
Unsafe.WriteUnaligned(ref output, packed.AsUInt32().ToScalar());
}
}
/// <inheritdoc/>
public static void StoreScalar(ref byte destination, int offset, int value) => Unsafe.Add(ref destination, offset) = (byte)value;
}
}

19
src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.CopyFilterOperator.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
internal static partial class Av1CdefFilter
{
/// <summary>
/// Disables both tap groups so the source block is copied unchanged.
/// </summary>
private readonly struct CopyFilterOperator : IFilterOperator
{
/// <inheritdoc/>
public static bool EnablePrimary => false;
/// <inheritdoc/>
public static bool EnableSecondary => false;
}
}

50
src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.Operator.cs

@ -0,0 +1,50 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
internal static partial class Av1CdefFilter
{
/// <summary>
/// Defines storage-specific writes for one filtered row.
/// </summary>
/// <typeparam name="TSample">The destination sample storage type.</typeparam>
private interface IOutputOperator<TSample>
where TSample : unmanaged
{
/// <summary>
/// Stores four or eight filtered samples from the low vector lanes.
/// </summary>
/// <param name="destination">The first element in the destination plane.</param>
/// <param name="offset">The offset of the first sample to write.</param>
/// <param name="value">The filtered samples in the low lanes.</param>
/// <param name="count">The number of valid lanes.</param>
public static abstract void StoreVector(ref TSample destination, int offset, Vector128<short> value, int count);
/// <summary>
/// Stores one filtered sample.
/// </summary>
/// <param name="destination">The first element in the destination plane.</param>
/// <param name="offset">The offset of the sample to write.</param>
/// <param name="value">The filtered sample.</param>
public static abstract void StoreScalar(ref TSample destination, int offset, int value);
}
/// <summary>
/// Defines which groups of directional taps participate in one closed filter kernel.
/// </summary>
private interface IFilterOperator
{
/// <summary>
/// Gets a value indicating whether the primary directional taps are enabled.
/// </summary>
public static abstract bool EnablePrimary { get; }
/// <summary>
/// Gets a value indicating whether the secondary off-axis taps are enabled.
/// </summary>
public static abstract bool EnableSecondary { get; }
}
}

19
src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.PrimaryAndSecondaryFilterOperator.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
internal static partial class Av1CdefFilter
{
/// <summary>
/// Enables both directional tap groups and their combined clipping rule.
/// </summary>
private readonly struct PrimaryAndSecondaryFilterOperator : IFilterOperator
{
/// <inheritdoc/>
public static bool EnablePrimary => true;
/// <inheritdoc/>
public static bool EnableSecondary => true;
}
}

19
src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.PrimaryFilterOperator.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
internal static partial class Av1CdefFilter
{
/// <summary>
/// Enables only the primary directional taps.
/// </summary>
private readonly struct PrimaryFilterOperator : IFilterOperator
{
/// <inheritdoc/>
public static bool EnablePrimary => true;
/// <inheritdoc/>
public static bool EnableSecondary => false;
}
}

19
src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.SecondaryFilterOperator.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
internal static partial class Av1CdefFilter
{
/// <summary>
/// Enables only the secondary off-axis taps.
/// </summary>
private readonly struct SecondaryFilterOperator : IFilterOperator
{
/// <inheritdoc/>
public static bool EnablePrimary => false;
/// <inheritdoc/>
public static bool EnableSecondary => true;
}
}

34
src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.UInt16OutputOperator.cs

@ -0,0 +1,34 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
internal static partial class Av1CdefFilter
{
/// <summary>
/// Writes filtered samples to 16-bit plane storage.
/// </summary>
private readonly struct UInt16OutputOperator : IOutputOperator<ushort>
{
/// <inheritdoc/>
public static void StoreVector(ref ushort destination, int offset, Vector128<short> value, int count)
{
ref ushort output = ref Unsafe.Add(ref destination, offset);
if (count == 8)
{
value.AsUInt16().StoreUnsafe(ref output);
}
else
{
ref byte outputBytes = ref Unsafe.As<ushort, byte>(ref output);
Unsafe.WriteUnaligned(ref outputBytes, value.AsUInt64().GetLower().ToScalar());
}
}
/// <inheritdoc/>
public static void StoreScalar(ref ushort destination, int offset, int value) => Unsafe.Add(ref destination, offset) = (ushort)value;
}
}

1694
src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.cs

File diff suppressed because it is too large

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

Loading…
Cancel
Save