Browse Source

Complete AV1 decoder audit remediation

pull/2633/head
James Jackson-South 1 day ago
parent
commit
7d8af2b968
  1. 37
      HEIF_IMPLEMENTATION_PLAN.md
  2. 2
      src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs
  3. 331
      src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs
  4. 265
      src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs
  5. 2
      src/ImageSharp/Formats/Heif/Av1/Color/Av1PlanarSampleBuffer.cs
  6. 91
      src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBuffer.cs
  7. 4
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs
  8. 41
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContexts.cs
  9. 13
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs
  10. 2
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorConfig.cs
  11. 20
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuConstraintDirectionalEnhancementFilterParameters.cs
  12. 2
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDecoderModelInfo.cs
  13. 2
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDeltaParameters.cs
  14. 2
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFilmGrainParameters.cs
  15. 11
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs
  16. 2
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameSize.cs
  17. 2
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuHeader.cs
  18. 31
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopFilterParameters.cs
  19. 4
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationItem.cs
  20. 16
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationParameters.cs
  21. 2
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOperatingPoint.cs
  22. 2
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOrderHintInfo.cs
  23. 38
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuQuantizationParameters.cs
  24. 481
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs
  25. 55
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSegmentationParameters.cs
  26. 15
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceHeader.cs
  27. 2
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSkipModeParameters.cs
  28. 27
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTileGroupHeader.cs
  29. 2
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTimingInfo.cs
  30. 4
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs
  31. 42
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs
  32. 705
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs
  33. 12
      src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainDecoder.cs
  34. 143
      src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterContext.cs
  35. 4
      src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterDecoder.cs
  36. 17
      src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationBoundary.cs
  37. 332
      src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationDecoder.cs
  38. 10
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1DeQuantizationContext.cs
  39. 2
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizationLookup.cs
  40. 4
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizer.cs
  41. 7
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1QuantizationLookup.cs
  42. 2
      src/ImageSharp/Formats/Heif/Av1/Pipeline/SuperResolution/Av1SuperResolutionDecoder.cs
  43. 136
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1BottomRightTopLeftConstants.cs
  44. 8
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalIntraPredictor.Operations.cs
  45. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.HorizontalOperator.cs
  46. 4
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.Operator.cs
  47. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.Packing.cs
  48. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.PaethOperator.cs
  49. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothHorizontalOperator.cs
  50. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothOperator.cs
  51. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothVerticalOperator.cs
  52. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.VerticalOperator.cs
  53. 16
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictorBase.cs
  54. 32
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs
  55. 6
      src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.Operations.cs
  56. 33
      src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.cs
  57. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundAveragePredictor.Operator.cs
  58. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundAveragePredictor.cs
  59. 38
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeightedPredictor.Operator.cs
  60. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeightedPredictor.cs
  61. 32
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Operator.cs
  62. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.cs
  63. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.Operator.cs
  64. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.cs
  65. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDifferenceWeightedMaskBuilder.Operator.cs
  66. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDifferenceWeightedMaskBuilder.cs
  67. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.Operator.cs
  68. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.cs
  69. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateMaskBlendPredictor.Operator.cs
  70. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateMaskBlendPredictor.cs
  71. 50
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundMaskBlendPredictor.Operator.cs
  72. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundMaskBlendPredictor.cs
  73. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1DifferenceWeightedMaskBuilder.Operator.cs
  74. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1DifferenceWeightedMaskBuilder.cs
  75. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterIntraMaskBuilder.cs
  76. 14
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.Operator.cs
  77. 8
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.cs
  78. 26
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Arithmetic.cs
  79. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.BilinearOperator.cs
  80. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Dispatch.cs
  81. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Filters.cs
  82. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.OneDimension.cs
  83. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Operator.cs
  84. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.RegularOperator.cs
  85. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.SharpOperator.cs
  86. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.SmoothOperator.cs
  87. 18
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.TwoDimensions.Byte.cs
  88. 10
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.TwoDimensions.UInt16.cs
  89. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.cs
  90. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.Filters.cs
  91. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.Operator.cs
  92. 2
      src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.cs
  93. 32
      src/ImageSharp/Formats/Heif/Av1/Readme.md
  94. 145
      src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrame.cs
  95. 6
      src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameBorder.cs
  96. 25
      src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameStore.cs
  97. 19
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs
  98. 360
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.MotionField.cs
  99. 656
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs
  100. 50
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameModeInfoMap.cs

37
HEIF_IMPLEMENTATION_PLAN.md

@ -572,7 +572,7 @@ Previously verified algorithm checkpoints remain valuable evidence, but the fina
- [ ] Verify still items and bounded sequences from file, memory, non-seekable, and short-read streams. - [ ] Verify still items and bounded sequences from file, memory, non-seekable, and short-read streams.
- [ ] Verify ICC, CICP, alpha, grids, pixel aspect ratio, clean aperture, rotation, mirroring, metadata, and every presented sequence frame. - [ ] Verify ICC, CICP, alpha, grids, pixel aspect ratio, clean aperture, rotation, mirroring, metadata, and every presented sequence frame.
- [ ] Complete the public AVIF format/API review so registered capabilities match implemented behavior. - [ ] Complete the public AVIF format/API review so registered capabilities match implemented behavior.
- [ ] Remove or reject every valid in-scope AV1 syntax branch that remains silently ignored or unsupported. - [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: Verified negative-path and frame-identifier gate evidence on 2026-08-31:
@ -589,6 +589,9 @@ Verified negative-path and frame-identifier gate evidence on 2026-08-31:
constrained multi-group allocation, motion-field allocation failure unwinding, and frame identifiers. 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 - [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. 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: Verified bounded-OBU checkpoint evidence on 2026-08-31:
@ -700,6 +703,38 @@ Verified retained-frame lifecycle checkpoint evidence on 2026-08-31:
reports zero compiler errors, scoped analyzer verification reports no changes, `git diff --check` reports zero compiler errors, scoped analyzer verification reports no changes, `git diff --check`
passes, and `.gitattributes` is unchanged. 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.
Decoder exit gate: Decoder exit gate:
- [ ] Every supported native format and AV1 tool has exact current-main libaom production-path evidence. - [ ] Every supported native format and AV1 tool has exact current-main libaom production-path evidence.

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

@ -551,7 +551,7 @@ internal sealed class Av1CodecConfiguration
/// <param name="sourceName">The source description used by invalid-content errors.</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="contentLightLevel">Receives decoded content light-level metadata when present.</param>
/// <param name="masteringDisplayColorVolume">Receives decoded mastering-display metadata when present.</param> /// <param name="masteringDisplayColorVolume">Receives decoded mastering-display metadata when present.</param>
private static void ReadHdrMetadata( public static void ReadHdrMetadata(
ReadOnlySpan<byte> payload, ReadOnlySpan<byte> payload,
string sourceName, string sourceName,
out HeifContentLightLevel? contentLightLevel, out HeifContentLightLevel? contentLightLevel,

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

@ -73,19 +73,9 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
private ObuSequenceHeader? validatedSequenceHeader; private ObuSequenceHeader? validatedSequenceHeader;
/// <summary> /// <summary>
/// The tile parser shared by all tile groups in the current frame. /// The complete parser, sample buffer, and reconstruction state for the frame currently being decoded.
/// </summary> /// </summary>
private Av1TileReader? tileReader; private FrameDecodeState? frameDecodeState;
/// <summary>
/// The destination sample buffer for the frame currently being parsed and reconstructed.
/// </summary>
private Av1FrameBuffer<byte>? frameBuffer;
/// <summary>
/// The reconstruction pipeline for the frame currently being parsed.
/// </summary>
private Av1FrameDecoder? frameDecoder;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1Decoder"/> class. /// Initializes a new instance of the <see cref="Av1Decoder"/> class.
@ -139,10 +129,16 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
public ObuSequenceHeader? SequenceHeader { get; private set; } public ObuSequenceHeader? SequenceHeader { get; private set; }
/// <summary> /// <summary>
/// Gets the tile and superblock state for the final retained shown frame, or <see langword="null"/> before one completes. /// 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> /// </summary>
public Av1FrameInfo? FrameInfo { get; private set; } 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> /// <summary>
/// Gets the native planes of the current retained shown frame, or <see langword="null"/> before one completes. /// Gets the native planes of the current retained shown frame, or <see langword="null"/> before one completes.
/// </summary> /// </summary>
@ -183,6 +179,15 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
CicpProfile = effectiveColorProfile 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 try
{ {
return new Image<TPixel>(this.configuration, metadata, [frame]); return new Image<TPixel>(this.configuration, metadata, [frame]);
@ -224,9 +229,10 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
containerColorProfile, containerColorProfile,
codecConfiguration, codecConfiguration,
out effectiveColorProfile, out effectiveColorProfile,
out ObuFrameHeader frameHeader,
layeredImageIndex); layeredImageIndex);
return this.ConvertToFrame<TPixel>(frameBuffer, effectiveColorProfile, presentationSize); return this.ConvertToFrame<TPixel>(frameBuffer, frameHeader, effectiveColorProfile, presentationSize);
} }
/// <summary> /// <summary>
@ -250,7 +256,8 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
null, null,
requireShownFrame: true); requireShownFrame: true);
return this.ConvertToFrame<TPixel>(this.referenceFrames.OutputFrame!.FrameBuffer, effectiveColorProfile); Av1ReferenceFrame outputFrame = this.referenceFrames.ResolveOutput();
return this.ConvertToFrame<TPixel>(outputFrame.FrameBuffer, outputFrame.FrameHeader, effectiveColorProfile);
} }
/// <summary> /// <summary>
@ -300,8 +307,9 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
null, null,
requireShownFrame: true); requireShownFrame: true);
Av1ReferenceFrame outputFrame = this.referenceFrames.ResolveOutput();
this.ComposeAlpha( this.ComposeAlpha(
this.referenceFrames.OutputFrame!.FrameBuffer, outputFrame.FrameBuffer,
expectedCodedSize, expectedCodedSize,
destination, destination,
outputSize, outputSize,
@ -314,11 +322,13 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
/// </summary> /// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam> /// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <param name="frameBuffer">The decoded native planes.</param> /// <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="effectiveColorProfile">The effective CICP description.</param>
/// <param name="presentationSize">The requested item presentation size, or an empty size for the coded dimensions.</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> /// <returns>The independently owned packed-pixel frame.</returns>
private ImageFrame<TPixel> ConvertToFrame<TPixel>( private ImageFrame<TPixel> ConvertToFrame<TPixel>(
Av1FrameBuffer<byte> frameBuffer, Av1FrameBuffer<byte> frameBuffer,
ObuFrameHeader frameHeader,
CicpProfile effectiveColorProfile, CicpProfile effectiveColorProfile,
Size presentationSize = default) Size presentationSize = default)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
@ -327,8 +337,8 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
try try
{ {
Size codedSize = new( Size codedSize = new(
this.FrameHeader!.FrameSize.SuperResolutionUpscaledWidth, frameHeader.FrameSize.SuperResolutionUpscaledWidth,
this.FrameHeader.FrameSize.FrameHeight); frameHeader.FrameSize.FrameHeight);
// A selected lower spatial layer can only be scaled upward to the image item's ispe extent here. // 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. // Other item-size corrections keep using the shared packed-pixel presentation path after decoding.
@ -452,6 +462,24 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
Av1CodecConfiguration? codecConfiguration, Av1CodecConfiguration? codecConfiguration,
out CicpProfile effectiveColorProfile, out CicpProfile effectiveColorProfile,
Av1LayeredImageIndex? layeredImageIndex = null) 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( effectiveColorProfile = this.DecodePayload(
buffer, buffer,
@ -461,6 +489,7 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
requireShownFrame: true); requireShownFrame: true);
using Av1ReferenceFrame outputFrame = this.referenceFrames.TakeOutput(); using Av1ReferenceFrame outputFrame = this.referenceFrames.TakeOutput();
frameHeader = outputFrame.FrameHeader;
return outputFrame.TakeFrameBuffer(); return outputFrame.TakeFrameBuffer();
} }
@ -483,8 +512,13 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
this.codecConfiguration = codecConfiguration; this.codecConfiguration = codecConfiguration;
this.containerColorProfile = containerColorProfile; this.containerColorProfile = containerColorProfile;
this.validatedSequenceHeader = null; this.validatedSequenceHeader = null;
this.obuReader.ResetMetadata();
this.SequenceHeader = null; this.SequenceHeader = null;
this.FrameHeader = 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?.ReleaseOwner();
this.FrameInfo = null; this.FrameInfo = null;
@ -493,7 +527,7 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
if (layeredImageIndex is null) if (layeredImageIndex is null)
{ {
Av1BitStreamReader reader = new(buffer); Av1BitStreamReader reader = new(buffer);
this.obuReader.ReadAll(ref reader, buffer.Length, () => this, false); this.obuReader.ReadAll(ref reader, buffer.Length, this, false);
} }
else else
{ {
@ -514,7 +548,7 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
int layerSize = (int)declaredLayerSize; int layerSize = (int)declaredLayerSize;
Av1BitStreamReader layerReader = new(buffer.Slice(layerOffset, layerSize)); Av1BitStreamReader layerReader = new(buffer.Slice(layerOffset, layerSize));
this.obuReader.ReadAll(ref layerReader, layerSize, () => this, false); this.obuReader.ReadAll(ref layerReader, layerSize, this, false);
layerOffset += layerSize; layerOffset += layerSize;
} }
@ -522,17 +556,16 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
{ {
Span<byte> finalLayer = buffer[layerOffset..]; Span<byte> finalLayer = buffer[layerOffset..];
Av1BitStreamReader finalLayerReader = new(finalLayer); Av1BitStreamReader finalLayerReader = new(finalLayer);
this.obuReader.ReadAll(ref finalLayerReader, finalLayer.Length, () => this, false); this.obuReader.ReadAll(ref finalLayerReader, finalLayer.Length, this, false);
} }
} }
ObuSequenceHeader sequenceHeader = this.obuReader.SequenceHeader!; ObuSequenceHeader sequenceHeader = this.obuReader.SequenceHeader
Guard.NotNull(sequenceHeader, nameof(sequenceHeader)); ?? throw new InvalidImageContentException("The AV1 payload contains no sequence header.");
if (requireShownFrame) if (requireShownFrame)
{ {
Guard.NotNull(this.referenceFrames.OutputFrame, nameof(this.referenceFrames.OutputFrame)); _ = this.referenceFrames.ResolveOutput();
Guard.NotNull(this.SequenceHeader, nameof(this.SequenceHeader));
Guard.NotNull(this.FrameHeader, nameof(this.FrameHeader));
} }
// Preserve the effective CICP description used for conversion, including container values that legally // Preserve the effective CICP description used for conversion, including container values that legally
@ -548,17 +581,14 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
{ {
// A failed frame may own pooled neighbor contexts while earlier layers own reconstructed references and // 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. // published CDF snapshots. None can be reused after a non-transactional frame transition has failed.
this.frameDecoder?.Dispose(); this.frameDecodeState?.Dispose();
this.frameDecoder = null; this.frameDecodeState = null;
this.frameBuffer?.Dispose();
this.frameBuffer = null;
this.tileReader?.Dispose();
this.tileReader = null;
this.obuReader.Reset(); this.obuReader.Reset();
this.entropyContexts?.Reset(); this.entropyContexts?.Reset();
this.entropySequenceHeader = null; this.entropySequenceHeader = null;
this.SequenceHeader = null; this.SequenceHeader = null;
this.FrameHeader = null; this.FrameHeader = null;
this.DecodedInterPredictionFeatures = Av1InterPredictionFeatures.None;
this.FrameInfo?.ReleaseOwner(); this.FrameInfo?.ReleaseOwner();
this.FrameInfo = null; this.FrameInfo = null;
throw; throw;
@ -584,6 +614,11 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
return; return;
} }
Av1FrameBuffer<byte>.ValidateDimensions(
sequenceHeader,
sequenceHeader.ColorConfig.GetColorFormat(),
false);
this.codecConfiguration?.Validate(sequenceHeader); this.codecConfiguration?.Validate(sequenceHeader);
CicpProfile? colorProfile = this.containerColorProfile; CicpProfile? colorProfile = this.containerColorProfile;
@ -640,12 +675,11 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
/// <param name="tileNum">The raster-order tile index.</param> /// <param name="tileNum">The raster-order tile index.</param>
public void ReadTile(Span<byte> tileData, int tileNum) public void ReadTile(Span<byte> tileData, int tileNum)
{ {
if (this.tileReader is null) FrameDecodeState frameDecodeState;
if (this.frameDecodeState is null)
{ {
ObuSequenceHeader? sequenceHeader = this.obuReader.SequenceHeader; ObuSequenceHeader sequenceHeader = this.obuReader.CurrentSequenceHeader;
ObuFrameHeader? frameHeader = this.obuReader.FrameHeader; ObuFrameHeader frameHeader = this.obuReader.CurrentFrameHeader;
Guard.NotNull(sequenceHeader, nameof(sequenceHeader));
Guard.NotNull(frameHeader, nameof(frameHeader));
this.ValidateSequence(sequenceHeader); this.ValidateSequence(sequenceHeader);
if (!ReferenceEquals(this.entropySequenceHeader, sequenceHeader)) if (!ReferenceEquals(this.entropySequenceHeader, sequenceHeader))
@ -680,27 +714,65 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
Av1FrameEntropyContexts entropyContexts = Av1FrameEntropyContexts entropyContexts =
this.entropyContexts ??= new(frameHeader.QuantizationParameters.BaseQIndex); this.entropyContexts ??= new(frameHeader.QuantizationParameters.BaseQIndex);
this.tileReader = new Av1TileReader( Av1TileReader? tileReader = null;
this.configuration, Av1FrameBuffer<byte>? frameBuffer = null;
sequenceHeader,
frameHeader, // Presentation-only samples contain no new tile syntax, so they keep the most recently reconstructed
entropyContexts, // frame state. Release that state only when a new reconstruction begins to avoid overlapping two graphs.
primaryReferenceContext, this.FrameInfo?.ReleaseOwner();
this.referenceFrames, this.FrameInfo = null;
this.lumaPaletteColorIndexMap,
this.chromaPaletteColorIndexMap); try
this.frameBuffer = new Av1FrameBuffer<byte>(this.configuration, sequenceHeader, sequenceHeader.ColorConfig.GetColorFormat(), false)
{ {
Width = frameHeader.FrameSize.FrameWidth, tileReader = new Av1TileReader(
Height = frameHeader.FrameSize.FrameHeight 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
};
this.frameDecoder = new Av1FrameDecoder(sequenceHeader, frameHeader, this.tileReader.FrameInfo, this.frameBuffer, this.referenceFrames); Av1FrameDecoder frameDecoder = new(
this.tileReader.FrameDecoder = this.frameDecoder; 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;
} }
this.tileReader.ReadTile(tileData, tileNum); frameDecodeState.TileReader.ReadTile(tileData, tileNum);
} }
/// <summary> /// <summary>
@ -708,12 +780,15 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
/// </summary> /// </summary>
public void CompleteFrame() public void CompleteFrame()
{ {
ObuSequenceHeader sequenceHeader = this.obuReader.SequenceHeader!; ObuSequenceHeader sequenceHeader = this.obuReader.SequenceHeader
ObuFrameHeader frameHeader = this.obuReader.FrameHeader!; ?? throw new InvalidImageContentException("An AV1 frame cannot complete before its sequence header.");
Av1FrameBuffer<byte>? frameBuffer = this.frameBuffer;
this.frameBuffer = null; ObuFrameHeader frameHeader = this.obuReader.FrameHeader
Av1FrameDecoder? frameDecoder = this.frameDecoder; ?? throw new InvalidImageContentException("An AV1 frame cannot complete before its frame header.");
this.frameDecoder = null;
Av1FrameBuffer<byte>? frameBuffer = null;
Av1FrameDecoder? frameDecoder = null;
Av1TileReader? tileReader = null;
Av1FrameBuffer<byte>? presentationBuffer = null; Av1FrameBuffer<byte>? presentationBuffer = null;
try try
@ -724,7 +799,6 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
{ {
Av1ReferenceFrame existingFrame = this.referenceFrames.ShowExisting((int)frameHeader.FrameToShowMapIdx); Av1ReferenceFrame existingFrame = this.referenceFrames.ShowExisting((int)frameHeader.FrameToShowMapIdx);
ObuFrameHeader existingFrameHeader = existingFrame.FrameHeader; ObuFrameHeader existingFrameHeader = existingFrame.FrameHeader;
Av1FrameInfo existingFrameInfo = existingFrame.FrameInfo;
if (existingFrameHeader.FrameType == ObuFrameType.KeyFrame) if (existingFrameHeader.FrameType == ObuFrameType.KeyFrame)
{ {
@ -739,33 +813,42 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
presentationBuffer = new Av1FrameBuffer<byte>( presentationBuffer = new Av1FrameBuffer<byte>(
this.configuration, this.configuration,
sequenceHeader, sequenceHeader,
sequenceHeader.ColorConfig.GetColorFormat(), existingFrame.FrameBuffer.ColorFormat,
false); false,
existingFrame.FrameBuffer.MaxWidth,
existingFrame.FrameBuffer.MaxHeight);
// Retained reference samples remain ungrained. Existing-frame presentation receives its own // 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. // allocator-owned copy only when the inherited film-grain parameters actually modify the output.
existingFrame.FrameBuffer.CopyTo(presentationBuffer); existingFrame.FrameBuffer.CopyVisibleTo(presentationBuffer);
Av1FilmGrainDecoder filmGrainDecoder = new(sequenceHeader, existingFrameHeader, presentationBuffer); Av1FilmGrainDecoder filmGrainDecoder = new(sequenceHeader, existingFrameHeader, presentationBuffer);
filmGrainDecoder.DecodeFrame(); filmGrainDecoder.DecodeFrame();
Av1ReferenceFrame presentationFrame = new(presentationBuffer, existingFrameHeader, existingFrameInfo); Av1ReferenceFrame presentationFrame = new(presentationBuffer, existingFrameHeader);
presentationBuffer = null; presentationBuffer = null;
this.referenceFrames.CommitOutput(presentationFrame); this.referenceFrames.CommitOutput(presentationFrame);
} }
this.SequenceHeader = sequenceHeader; this.SequenceHeader = sequenceHeader;
this.FrameHeader = existingFrameHeader; this.FrameHeader = existingFrameHeader;
existingFrameInfo.AddOwner();
this.FrameInfo?.ReleaseOwner();
this.FrameInfo = existingFrameInfo;
return; return;
} }
Av1TileReader tileReader = this.tileReader!; 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; Av1FrameInfo frameInfo = tileReader.FrameInfo;
Av1FrameBuffer<byte> reconstructedFrameBuffer = frameBuffer!; Av1FrameBuffer<byte> reconstructedFrameBuffer = frameBuffer;
frameDecoder!.CompleteFrame(); frameDecoder.CompleteFrame();
bool retainsReference = (frameHeader.RefreshFrameFlags & byte.MaxValue) != 0; bool retainsReference = (frameHeader.RefreshFrameFlags & byte.MaxValue) != 0;
if (retainsReference) if (retainsReference)
@ -773,6 +856,10 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
// Motion compensation may address any clamped position inside the decoder border. Extending once after // 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. // all in-loop filters lets every later block use the full padded span without per-prediction edge copies.
Av1ReferenceFrameBorder.Extend(reconstructedFrameBuffer); 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; bool needsSeparatePresentation = frameHeader.ShowFrame && frameHeader.FilmGrainParameters.ApplyGrain && retainsReference;
@ -781,12 +868,14 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
presentationBuffer = new Av1FrameBuffer<byte>( presentationBuffer = new Av1FrameBuffer<byte>(
this.configuration, this.configuration,
sequenceHeader, sequenceHeader,
sequenceHeader.ColorConfig.GetColorFormat(), reconstructedFrameBuffer.ColorFormat,
false); false,
reconstructedFrameBuffer.MaxWidth,
reconstructedFrameBuffer.MaxHeight);
// Film grain must never contaminate a decoded reference. A shown frame that is also refreshed therefore // 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. // receives one allocator-owned presentation copy; frames with no reference role are grained in place.
reconstructedFrameBuffer.CopyTo(presentationBuffer); reconstructedFrameBuffer.CopyVisibleTo(presentationBuffer);
} }
Av1FrameBuffer<byte> grainTarget = presentationBuffer ?? reconstructedFrameBuffer; Av1FrameBuffer<byte> grainTarget = presentationBuffer ?? reconstructedFrameBuffer;
@ -799,14 +888,24 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
Av1ReferenceFrame referenceFrame; Av1ReferenceFrame referenceFrame;
if (retainsReference) if (retainsReference)
{ {
Av1FrameEntropyContexts entropyContexts = this.entropyContexts!; Av1FrameEntropyContexts entropyContexts = tileReader.EntropyContexts;
Av1FrameEntropyContext entropySnapshot = entropyContexts.RentPublishedSnapshot(); Av1FrameEntropyContext entropySnapshot = entropyContexts.RentPublishedSnapshot();
referenceFrame = new(reconstructedFrameBuffer, frameHeader, frameInfo, entropySnapshot, entropyContexts); 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 else
{ {
// Presentation-only frames can never become primary references, so they own no unused CDF graph. // Presentation-only frames can never become primary references, so they own no unused CDF graph.
referenceFrame = new(reconstructedFrameBuffer, frameHeader, frameInfo); referenceFrame = new(reconstructedFrameBuffer, frameHeader);
} }
frameBuffer = null; frameBuffer = null;
@ -818,34 +917,32 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
if (presentationBuffer is not null) if (presentationBuffer is not null)
{ {
Av1ReferenceFrame presentationFrame = new(presentationBuffer, frameHeader, frameInfo); Av1ReferenceFrame presentationFrame = new(presentationBuffer, frameHeader);
presentationBuffer = null; presentationBuffer = null;
this.referenceFrames.CommitOutput(presentationFrame); this.referenceFrames.CommitOutput(presentationFrame);
} }
if (frameHeader.ShowFrame) this.SequenceHeader = sequenceHeader;
{ this.FrameHeader = frameHeader;
this.SequenceHeader = sequenceHeader; this.DecodedInterPredictionFeatures |= frameInfo.InterPredictionFeatures;
this.FrameHeader = frameHeader;
// Hidden frames can contain the inter syntax needed to validate a sequence. Retain only the latest full
// The output frame owner is released after its sample buffer transfers to the caller. Retain the // reconstruction state until the next bounded decode; reference-map entries keep their compact state.
// parsed state independently so diagnostics and conformance inspection remain valid until the next frameInfo.AddOwner();
// bounded decode or decoder disposal. this.FrameInfo?.ReleaseOwner();
frameInfo.AddOwner(); this.FrameInfo = frameInfo;
this.FrameInfo?.ReleaseOwner();
this.FrameInfo = frameInfo;
}
} }
finally finally
{ {
// A non-shown frame or failed reconstruction never escapes this callback. The tile reader releases only // A non-shown frame or failed reconstruction never escapes this callback. The tile reader releases the
// its initial frame-state lease; retained frames and the decoder result keep allocator-owned motion fields // reconstruction lease; a retained frame keeps only its compact reference state after neighbor contexts
// alive independently after the entropy-neighbor contexts are returned. // and the remaining frame-sized syntax are returned.
frameDecoder?.Dispose(); frameDecoder?.Dispose();
presentationBuffer?.Dispose(); presentationBuffer?.Dispose();
frameBuffer?.Dispose(); frameBuffer?.Dispose();
this.tileReader?.Dispose(); tileReader?.Dispose();
this.tileReader = null; this.frameDecodeState?.Dispose();
this.frameDecodeState = null;
} }
} }
@ -854,16 +951,46 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
this.frameDecoder?.Dispose(); this.frameDecodeState?.Dispose();
this.frameDecoder = null; this.frameDecodeState = null;
this.frameBuffer?.Dispose();
this.frameBuffer = null;
this.tileReader?.Dispose();
this.tileReader = null;
this.referenceFrames.Dispose(); this.referenceFrames.Dispose();
this.FrameInfo?.ReleaseOwner(); this.FrameInfo?.ReleaseOwner();
this.FrameInfo = null; this.FrameInfo = null;
this.lumaPaletteColorIndexMap.Dispose(); this.lumaPaletteColorIndexMap.Dispose();
this.chromaPaletteColorIndexMap.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();
}
}
} }

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

@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// Owns the padded luma and chroma sample planes for one decoded AV1 frame. /// Owns the padded luma and chroma sample planes for one decoded AV1 frame.
/// </summary> /// </summary>
/// <typeparam name="T">The unmanaged storage-element type used by the plane allocations.</typeparam> /// <typeparam name="T">The unmanaged storage-element type used by the plane allocations.</typeparam>
internal class Av1FrameBuffer<T> : IDisposable internal sealed class Av1FrameBuffer<T> : IDisposable
where T : unmanaged where T : unmanaged
{ {
/// <summary> /// <summary>
@ -53,7 +53,12 @@ internal class Av1FrameBuffer<T> : IDisposable
private readonly int storageElementsPerSample; private readonly int storageElementsPerSample;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1FrameBuffer{T}"/> class. /// 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> /// </summary>
/// <param name="configuration">The configuration providing the plane allocator.</param> /// <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="sequenceHeader">The sequence header defining maximum dimensions, bit depth, and chroma layout.</param>
@ -61,11 +66,40 @@ internal class Av1FrameBuffer<T> : IDisposable
/// <param name="is16BitPipeline">Indicates whether reconstruction uses native 16-bit sample storage.</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> /// <exception cref="InvalidImageContentException">The padded frame planes cannot be represented as contiguous allocations.</exception>
public Av1FrameBuffer(Configuration configuration, ObuSequenceHeader sequenceHeader, Av1ColorFormat maxColorFormat, bool is16BitPipeline) 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; this.MemoryAllocator = configuration.MemoryAllocator;
Av1ColorFormat colorFormat = sequenceHeader.ColorConfig.IsMonochrome ? Av1ColorFormat.Yuv400 : maxColorFormat; Av1ColorFormat colorFormat = sequenceHeader.ColorConfig.IsMonochrome ? Av1ColorFormat.Yuv400 : maxColorFormat;
this.MaxWidth = sequenceHeader.MaxFrameWidth; this.MaxWidth = allocationWidth;
this.MaxHeight = sequenceHeader.MaxFrameHeight; this.MaxHeight = allocationHeight;
this.BitDepth = sequenceHeader.ColorConfig.BitDepth; this.BitDepth = sequenceHeader.ColorConfig.BitDepth;
this.ColorConfig = sequenceHeader.ColorConfig; this.ColorConfig = sequenceHeader.ColorConfig;
this.BytesPerSample = this.BitDepth > Av1BitDepth.EightBit || is16BitPipeline ? 2 : 1; this.BytesPerSample = this.BitDepth > Av1BitDepth.EightBit || is16BitPipeline ? 2 : 1;
@ -121,34 +155,42 @@ internal class Av1FrameBuffer<T> : IDisposable
throw new InvalidImageContentException("The AV1 frame dimensions exceed the contiguous decoder plane limit."); throw new InvalidImageContentException("The AV1 frame dimensions exceed the contiguous decoder plane limit.");
} }
this.BufferY = null;
this.BufferCb = null;
this.BufferCr = null;
// Block reconstruction and the SIMD predictors address decoder padding through one span plus a constant row // 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. // 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 try
{ {
if ((bufferEnableMask & PictureBufferYFlag) != 0) ChromaPlanes? chroma = null;
if (bufferEnableMask == PictureBufferFullMask)
{ {
this.BufferY = configuration.MemoryAllocator.Allocate2D<T>(strideY * this.storageElementsPerSample, heightY, preferContiguosImageBuffers: true); chromaBlue = configuration.MemoryAllocator.Allocate2D<T>(
} strideChroma * this.storageElementsPerSample,
heightChroma,
preferContiguosImageBuffers: true);
if ((bufferEnableMask & PictureBufferCbFlag) != 0) chromaRed = configuration.MemoryAllocator.Allocate2D<T>(
{ strideChroma * this.storageElementsPerSample,
this.BufferCb = configuration.MemoryAllocator.Allocate2D<T>(strideChroma * this.storageElementsPerSample, heightChroma, preferContiguosImageBuffers: true); heightChroma,
} preferContiguosImageBuffers: true);
if ((bufferEnableMask & PictureBufferCrFlag) != 0) chroma = new ChromaPlanes(chromaBlue, chromaRed);
{
this.BufferCr = configuration.MemoryAllocator.Allocate2D<T>(strideChroma * this.storageElementsPerSample, heightChroma, preferContiguosImageBuffers: true);
} }
this.planes = new(luma, chroma);
} }
catch catch
{ {
// Construction publishes the owner only after every required plane has been rented. Release earlier planes // 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. // here because a later allocation failure leaves no constructed frame buffer for the caller to dispose.
this.Dispose(); chromaRed?.Dispose();
chromaBlue?.Dispose();
luma.Dispose();
throw; throw;
} }
} }
@ -161,17 +203,17 @@ internal class Av1FrameBuffer<T> : IDisposable
/// <summary> /// <summary>
/// Gets the Y luma buffer. /// Gets the Y luma buffer.
/// </summary> /// </summary>
public Buffer2D<T>? BufferY { get; private set; } public Buffer2D<T>? BufferY => this.planes?.Luma;
/// <summary> /// <summary>
/// Gets the U chroma buffer. /// Gets the U chroma buffer.
/// </summary> /// </summary>
public Buffer2D<T>? BufferCb { get; private set; } public Buffer2D<T>? BufferCb => this.planes?.Chroma?.Blue;
/// <summary> /// <summary>
/// Gets the V chroma buffer. /// Gets the V chroma buffer.
/// </summary> /// </summary>
public Buffer2D<T>? BufferCr { get; private set; } public Buffer2D<T>? BufferCr => this.planes?.Chroma?.Red;
/// <summary> /// <summary>
/// Gets or sets the horizontal padding distance. /// Gets or sets the horizontal padding distance.
@ -234,22 +276,71 @@ internal class Av1FrameBuffer<T> : IDisposable
public MemoryAllocator MemoryAllocator { get; } public MemoryAllocator MemoryAllocator { get; }
/// <summary> /// <summary>
/// Copies the complete padded sample planes and active picture geometry to another compatible frame buffer. /// Validates that the maximum sequence planes fit the decoder's contiguous ownership contract.
/// </summary> /// </summary>
/// <param name="destination">The frame buffer receiving the copied reconstruction.</param> /// <param name="sequenceHeader">The sequence header defining maximum dimensions, bit depth, and chroma layout.</param>
public void CopyTo(Av1FrameBuffer<T> destination) /// <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)
{ {
// Copy each contiguous allocation so the runtime can use its optimized bulk-memory path. Film-grain int bytesPerSample = sequenceHeader.ColorConfig.BitDepth > Av1BitDepth.EightBit || is16BitPipeline ? 2 : 1;
// presentation consumes right and bottom padding for odd dimensions, so copying only visible rows would leave int storageElementsPerSample = Math.Max(
// part of the independently owned presentation surface undefined. (bytesPerSample + Unsafe.SizeOf<T>() - 1) / Unsafe.SizeOf<T>(),
this.BufferY!.DangerousGetSingleSpan().CopyTo(destination.BufferY!.DangerousGetSingleSpan()); 1);
Buffer2D<T>? chromaBlue = this.BufferCb;
if (chromaBlue is not null) 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)
{ {
chromaBlue.DangerousGetSingleSpan().CopyTo(destination.BufferCb!.DangerousGetSingleSpan()); case Av1ColorFormat.Yuv420:
this.BufferCr!.DangerousGetSingleSpan().CopyTo(destination.BufferCr!.DangerousGetSingleSpan()); 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.StartPosition = this.StartPosition;
destination.OriginX = this.OriginX; destination.OriginX = this.OriginX;
destination.OriginY = this.OriginY; destination.OriginY = this.OriginY;
@ -259,6 +350,47 @@ internal class Av1FrameBuffer<T> : IDisposable
destination.MaxHeight = this.MaxHeight; destination.MaxHeight = this.MaxHeight;
destination.BitDepth = this.BitDepth; destination.BitDepth = this.BitDepth;
destination.ColorFormat = this.ColorFormat; 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> /// <summary>
@ -266,12 +398,21 @@ internal class Av1FrameBuffer<T> : IDisposable
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
this.BufferY?.Dispose(); FramePlanes? ownedPlanes = this.planes;
this.BufferY = null; this.planes = null;
this.BufferCb?.Dispose(); if (ownedPlanes is null)
this.BufferCb = null; {
this.BufferCr?.Dispose(); return;
this.BufferCr = null; }
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> /// <summary>
@ -447,19 +588,23 @@ internal class Av1FrameBuffer<T> : IDisposable
out int width, out int width,
out int height) out int height)
{ {
FramePlanes? ownedPlanes = this.planes;
ObjectDisposedException.ThrowIf(ownedPlanes is null, this);
FramePlanes activePlanes = ownedPlanes.Value;
switch (plane) switch (plane)
{ {
case Av1Plane.Y: case Av1Plane.Y:
Guard.NotNull(this.BufferY); buffer = activePlanes.Luma;
buffer = this.BufferY;
originX = this.OriginX; originX = this.OriginX;
originY = this.OriginY; originY = this.OriginY;
width = this.Width; width = this.Width;
height = this.Height; height = this.Height;
break; break;
case Av1Plane.U: case Av1Plane.U:
Guard.NotNull(this.BufferCb); buffer = activePlanes.Chroma?.Blue
buffer = this.BufferCb; ?? throw new InvalidOperationException("A monochrome AV1 frame has no blue-difference plane.");
originX = this.OriginX >> subX; originX = this.OriginX >> subX;
originY = this.OriginY >> subY; originY = this.OriginY >> subY;
width = Av1Math.DivideLog2Ceiling(this.Width, subX); width = Av1Math.DivideLog2Ceiling(this.Width, subX);
@ -467,8 +612,9 @@ internal class Av1FrameBuffer<T> : IDisposable
break; break;
case Av1Plane.V: case Av1Plane.V:
default: default:
Guard.NotNull(this.BufferCr); buffer = activePlanes.Chroma?.Red
buffer = this.BufferCr; ?? throw new InvalidOperationException("A monochrome AV1 frame has no red-difference plane.");
originX = this.OriginX >> subX; originX = this.OriginX >> subX;
originY = this.OriginY >> subY; originY = this.OriginY >> subY;
width = Av1Math.DivideLog2Ceiling(this.Width, subX); width = Av1Math.DivideLog2Ceiling(this.Width, subX);
@ -476,4 +622,33 @@ internal class Av1FrameBuffer<T> : IDisposable
break; 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;
}
} }

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

@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Color;
/// Adapts reconstructed AV1 planes to the shared HEIF planar color pipeline. /// Adapts reconstructed AV1 planes to the shared HEIF planar color pipeline.
/// </summary> /// </summary>
/// <typeparam name="TSample">The native unsigned sample storage type.</typeparam> /// <typeparam name="TSample">The native unsigned sample storage type.</typeparam>
internal struct Av1PlanarSampleBuffer<TSample> : IHeifPlanarSampleBuffer<TSample> internal readonly struct Av1PlanarSampleBuffer<TSample> : IHeifPlanarSampleBuffer<TSample>
where TSample : unmanaged where TSample : unmanaged
{ {
/// <summary> /// <summary>

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

@ -25,19 +25,9 @@ internal sealed class Av1PresentationSampleBuffer<TSample, TBuffer> : IDisposabl
private readonly MemoryAllocator memoryAllocator; private readonly MemoryAllocator memoryAllocator;
/// <summary> /// <summary>
/// The scaled luma plane. /// The complete set of owned presentation planes, or <see langword="null"/> after disposal.
/// </summary> /// </summary>
private Buffer2D<TSample>? luma; private PresentationPlanes? planes;
/// <summary>
/// The scaled blue-difference plane.
/// </summary>
private Buffer2D<TSample>? chromaBlue;
/// <summary>
/// The scaled red-difference plane.
/// </summary>
private Buffer2D<TSample>? chromaRed;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1PresentationSampleBuffer{TSample, TBuffer}"/> class. /// Initializes a new instance of the <see cref="Av1PresentationSampleBuffer{TSample, TBuffer}"/> class.
@ -64,23 +54,34 @@ internal sealed class Av1PresentationSampleBuffer<TSample, TBuffer> : IDisposabl
int destinationChromaWidth = DivideCeiling(width, 1 << source.ChromaSubsamplingX); int destinationChromaWidth = DivideCeiling(width, 1 << source.ChromaSubsamplingX);
int destinationChromaHeight = DivideCeiling(height, 1 << source.ChromaSubsamplingY); int destinationChromaHeight = DivideCeiling(height, 1 << source.ChromaSubsamplingY);
Buffer2D<TSample>? luma = null;
Buffer2D<TSample>? chromaBlue = null;
Buffer2D<TSample>? chromaRed = null;
try try
{ {
this.luma = this.memoryAllocator.Allocate2D<TSample>(width, height); luma = this.memoryAllocator.Allocate2D<TSample>(width, height);
this.ScalePlane(source, Av1Plane.Y, source.Width, source.Height, this.luma); this.ScalePlane(source, Av1Plane.Y, source.Width, source.Height, luma);
ChromaPlanes? chroma = null;
if (!source.IsMonochrome) if (!source.IsMonochrome)
{ {
this.chromaBlue = this.memoryAllocator.Allocate2D<TSample>(destinationChromaWidth, destinationChromaHeight); chromaBlue = this.memoryAllocator.Allocate2D<TSample>(destinationChromaWidth, destinationChromaHeight);
this.ScalePlane(source, Av1Plane.U, sourceChromaWidth, sourceChromaHeight, this.chromaBlue); this.ScalePlane(source, Av1Plane.U, sourceChromaWidth, sourceChromaHeight, chromaBlue);
this.chromaRed = this.memoryAllocator.Allocate2D<TSample>(destinationChromaWidth, destinationChromaHeight); chromaRed = this.memoryAllocator.Allocate2D<TSample>(destinationChromaWidth, destinationChromaHeight);
this.ScalePlane(source, Av1Plane.V, sourceChromaWidth, sourceChromaHeight, this.chromaRed); 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 catch
{ {
this.Dispose(); luma?.Dispose();
chromaBlue?.Dispose();
chromaRed?.Dispose();
throw; throw;
} }
} }
@ -140,12 +141,21 @@ internal sealed class Av1PresentationSampleBuffer<TSample, TBuffer> : IDisposabl
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
this.luma?.Dispose(); PresentationPlanes? planes = this.planes;
this.chromaBlue?.Dispose(); this.planes = null;
this.chromaRed?.Dispose();
this.luma = null; if (planes is null)
this.chromaBlue = null; {
this.chromaRed = 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> /// <summary>
@ -155,13 +165,22 @@ internal sealed class Av1PresentationSampleBuffer<TSample, TBuffer> : IDisposabl
/// <param name="row">The zero-based plane row.</param> /// <param name="row">The zero-based plane row.</param>
/// <returns>The visible samples in the requested row.</returns> /// <returns>The visible samples in the requested row.</returns>
public Span<TSample> GetRowSpan(Av1Plane plane, int row) public Span<TSample> GetRowSpan(Av1Plane plane, int row)
=> plane switch {
PresentationPlanes planes = this.planes
?? throw new ObjectDisposedException(nameof(Av1PresentationSampleBuffer<TSample, TBuffer>));
Buffer2D<TSample> buffer = plane switch
{ {
Av1Plane.Y => this.luma!.DangerousGetRowSpan(row), Av1Plane.Y => planes.Luma,
Av1Plane.U => this.chromaBlue!.DangerousGetRowSpan(row), Av1Plane.U => planes.Chroma?.Blue
_ => this.chromaRed!.DangerousGetRowSpan(row) ?? 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> /// <summary>
/// Scales one component plane with the native integer filter used by pinned libavif's libyuv backend. /// Scales one component plane with the native integer filter used by pinned libavif's libyuv backend.
/// </summary> /// </summary>
@ -742,4 +761,18 @@ internal sealed class Av1PresentationSampleBuffer<TSample, TBuffer> : IDisposabl
/// <param name="divisor">The positive divisor.</param> /// <param name="divisor">The positive divisor.</param>
/// <returns>The ceiling-rounded quotient.</returns> /// <returns>The ceiling-rounded quotient.</returns>
private static int DivideCeiling(int value, int divisor) => (value + divisor - 1) / divisor; 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;
}
} }

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

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
/// <summary> /// <summary>
/// Stores and adapts an AV1 inverse cumulative distribution used by the range coder. /// Stores and adapts an AV1 inverse cumulative distribution used by the range coder.
/// </summary> /// </summary>
internal class Av1Distribution internal sealed class Av1Distribution
{ {
/// <summary> /// <summary>
/// The exclusive upper bound of the Q15 probability domain. /// The exclusive upper bound of the Q15 probability domain.
@ -271,7 +271,7 @@ internal class Av1Distribution
/// </summary> /// </summary>
/// <param name="props">The forward Q15 thresholds followed by a zero sentinel slot.</param> /// <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> /// <param name="speed">The symbol-count contribution to the update rate.</param>
private Av1Distribution(uint[] props, int speed) private Av1Distribution(ReadOnlySpan<uint> props, int speed)
{ {
this.probabilities = new uint[props.Length]; this.probabilities = new uint[props.Length];

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

@ -21,11 +21,6 @@ internal sealed class Av1FrameEntropyContexts
/// </summary> /// </summary>
private InlineArray10<Av1FrameEntropyContext?> returnedSnapshots; private InlineArray10<Av1FrameEntropyContext?> returnedSnapshots;
/// <summary>
/// The number of returned snapshot graphs currently available for reuse.
/// </summary>
private int returnedSnapshotCount;
/// <summary> /// <summary>
/// The base quantizer index used to initialize a newly required snapshot graph. /// The base quantizer index used to initialize a newly required snapshot graph.
/// </summary> /// </summary>
@ -105,20 +100,24 @@ internal sealed class Av1FrameEntropyContexts
/// <returns>The snapshot that must later be returned through <see cref="ReturnSnapshot"/>.</returns> /// <returns>The snapshot that must later be returned through <see cref="ReturnSnapshot"/>.</returns>
public Av1FrameEntropyContext RentPublishedSnapshot() public Av1FrameEntropyContext RentPublishedSnapshot()
{ {
Av1FrameEntropyContext snapshot; Av1FrameEntropyContext? snapshot = null;
if (this.returnedSnapshotCount == 0) for (int snapshotIndex = 0; snapshotIndex < MaximumSnapshotCount; snapshotIndex++)
{
// Eight slots can own distinct frames while the selected output owns a ninth frame no longer present in
// the map. Rent one further graph before commit releases the owner displaced by the completed frame.
snapshot = new(this.currentQIndex);
}
else
{ {
int snapshotIndex = --this.returnedSnapshotCount; Av1FrameEntropyContext? returnedSnapshot = this.returnedSnapshots[snapshotIndex];
snapshot = this.returnedSnapshots[snapshotIndex]!; if (returnedSnapshot is null)
{
continue;
}
this.returnedSnapshots[snapshotIndex] = null; 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); this.Published.SnapshotTo(snapshot);
return snapshot; return snapshot;
} }
@ -130,9 +129,15 @@ internal sealed class Av1FrameEntropyContexts
public void ReturnSnapshot(Av1FrameEntropyContext snapshot) public void ReturnSnapshot(Av1FrameEntropyContext snapshot)
{ {
// The fixed capacity covers eight distinct slot owners, one detached presentation owner, and the replacement // The fixed capacity covers eight distinct slot owners, one detached presentation owner, and the replacement
// frame rented before commit. Av1ReferenceFrame returns each graph exactly once, so the session cannot exceed // frame rented before commit. Av1ReferenceFrame returns each graph exactly once, so one slot is always free.
// this bound. for (int snapshotIndex = 0; snapshotIndex < MaximumSnapshotCount; snapshotIndex++)
this.returnedSnapshots[this.returnedSnapshotCount++] = snapshot; {
if (this.returnedSnapshots[snapshotIndex] is null)
{
this.returnedSnapshots[snapshotIndex] = snapshot;
return;
}
}
} }
/// <summary> /// <summary>

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

@ -1271,19 +1271,6 @@ internal ref struct Av1SymbolDecoder
return r.ReadSymbol(this.context.EndOfBlockExtra[(int)transformSizeContext][(int)planeType][endOfBlockContext]) > 0; return r.ReadSymbol(this.context.EndOfBlockExtra[(int)transformSizeContext][(int)planeType][endOfBlockContext]) > 0;
} }
/// <summary>
/// Reads one coefficient base-range symbol.
/// </summary>
/// <param name="transformSizeContext">The square transform-size probability context.</param>
/// <param name="planeType">The luma or chroma plane category.</param>
/// <param name="baseRangeContext">The coefficient base-range context.</param>
/// <returns>The decoded base-range symbol.</returns>
private int ReadCoefficientsBaseRange(Av1TransformSize transformSizeContext, Av1PlaneType planeType, int baseRangeContext)
{
ref Av1SymbolReader r = ref this.reader;
return r.ReadSymbol(this.context.CoefficientsBaseRange[(int)transformSizeContext][(int)planeType][baseRangeContext]);
}
/// <summary> /// <summary>
/// Reads the sign of a nonzero DC coefficient. /// Reads the sign of a nonzero DC coefficient.
/// </summary> /// </summary>

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

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Contains the color configuration signaled by an AV1 sequence header. /// Contains the color configuration signaled by an AV1 sequence header.
/// </summary> /// </summary>
internal class ObuColorConfig internal sealed class ObuColorConfig
{ {
/// <summary> /// <summary>
/// Stores whether the sequence uses a single monochrome plane. /// Stores whether the sequence uses a single monochrome plane.

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

@ -6,8 +6,18 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Contains the constrained directional enhancement filter parameters for an AV1 frame. /// Contains the constrained directional enhancement filter parameters for an AV1 frame.
/// </summary> /// </summary>
internal class ObuConstraintDirectionalEnhancementFilterParameters 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> /// <summary>
/// Gets or sets the number of bits used to select a filter-strength entry. /// Gets or sets the number of bits used to select a filter-strength entry.
/// </summary> /// </summary>
@ -19,12 +29,12 @@ internal class ObuConstraintDirectionalEnhancementFilterParameters
public int Damping { get; set; } = 3; public int Damping { get; set; } = 3;
/// <summary> /// <summary>
/// Gets or sets the primary and secondary luma strengths for each filter entry. /// Gets the primary and secondary luma strengths for each filter entry.
/// </summary> /// </summary>
public int[] YStrength { get; set; } = new int[16]; public Span<int> YStrength => this.yStrength;
/// <summary> /// <summary>
/// Gets or sets the primary and secondary chroma strengths for each filter entry. /// Gets the primary and secondary chroma strengths for each filter entry.
/// </summary> /// </summary>
public int[] UvStrength { get; set; } = new int[16]; public Span<int> UvStrength => this.uvStrength;
} }

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

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary> /// <summary>
/// Contains the field widths and decoding-clock units used by the AV1 decoder model. /// Contains the field widths and decoding-clock units used by the AV1 decoder model.
/// </summary> /// </summary>
internal class ObuDecoderModelInfo internal sealed class ObuDecoderModelInfo
{ {
/// <summary> /// <summary>
/// Gets or sets BufferDelayLength. Specifies the length of the decoder_buffer_delay and the encoder_buffer_delay /// Gets or sets BufferDelayLength. Specifies the length of the decoder_buffer_delay and the encoder_buffer_delay

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

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Contains the delta-quantizer or delta-loop-filter signaling parameters for an AV1 frame. /// Contains the delta-quantizer or delta-loop-filter signaling parameters for an AV1 frame.
/// </summary> /// </summary>
internal class ObuDeltaParameters internal sealed class ObuDeltaParameters
{ {
/// <summary> /// <summary>
/// Gets or sets a value indicating whether per-block delta values are present. /// Gets or sets a value indicating whether per-block delta values are present.

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

@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Contains the parameters used to synthesize AV1 film grain for a decoded frame. /// Contains the parameters used to synthesize AV1 film grain for a decoded frame.
/// </summary> /// </summary>
internal class ObuFilmGrainParameters internal sealed class ObuFilmGrainParameters
{ {
/// <summary> /// <summary>
/// Stores the luma scaling-point coordinates without a per-frame array allocation. /// Stores the luma scaling-point coordinates without a per-frame array allocation.

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

@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Stores the decoded syntax and derived state for one AV1 frame header. /// Stores the decoded syntax and derived state for one AV1 frame header.
/// </summary> /// </summary>
internal class ObuFrameHeader internal sealed class ObuFrameHeader
{ {
/// <summary> /// <summary>
/// Stores the validity state of the eight reference-frame slots without a per-header array allocation. /// Stores the validity state of the eight reference-frame slots without a per-header array allocation.
@ -38,6 +38,11 @@ internal class ObuFrameHeader
/// </summary> /// </summary>
private InlineArray7<Av1GlobalMotionParameters> globalMotionParameters; 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> /// <summary>
/// Gets or sets the temporal-layer identifier carried by the primary frame-header OBU. /// Gets or sets the temporal-layer identifier carried by the primary frame-header OBU.
/// </summary> /// </summary>
@ -89,9 +94,9 @@ internal class ObuFrameHeader
public bool CodedLossless { get; set; } public bool CodedLossless { get; set; }
/// <summary> /// <summary>
/// Gets or sets the lossless-coding flag for each segment. /// Gets the mutable lossless-coding flags for each segment.
/// </summary> /// </summary>
public bool[] LosslessArray { get; set; } = new bool[Av1Constants.MaxSegmentCount]; public Span<bool> LosslessArray => this.losslessArray;
/// <summary> /// <summary>
/// Gets or sets the frame quantization parameters. /// Gets or sets the frame quantization parameters.

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

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Contains the coded, upscaled, and rendered dimensions of an AV1 frame. /// Contains the coded, upscaled, and rendered dimensions of an AV1 frame.
/// </summary> /// </summary>
internal class ObuFrameSize internal sealed class ObuFrameSize
{ {
/// <summary> /// <summary>
/// Gets or sets the coded frame width. /// Gets or sets the coded frame width.

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

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Contains the parsed header and payload size of an AV1 open bitstream unit. /// Contains the parsed header and payload size of an AV1 open bitstream unit.
/// </summary> /// </summary>
internal class ObuHeader internal sealed class ObuHeader
{ {
/// <summary> /// <summary>
/// Gets or sets the number of bytes occupied by the fixed OBU header and optional extension. /// Gets or sets the number of bytes occupied by the fixed OBU header and optional extension.

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

@ -6,22 +6,39 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Contains the AV1 deblocking-loop-filter parameters for a frame. /// Contains the AV1 deblocking-loop-filter parameters for a frame.
/// </summary> /// </summary>
internal class ObuLoopFilterParameters 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> /// <summary>
/// Initializes a new instance of the <see cref="ObuLoopFilterParameters"/> class with the AV1 default reference and mode deltas. /// Initializes a new instance of the <see cref="ObuLoopFilterParameters"/> class with the AV1 default reference and mode deltas.
/// </summary> /// </summary>
public ObuLoopFilterParameters() public ObuLoopFilterParameters()
{ {
// AV1 indexes this table from Intra through Alternate. Golden is -1; Backward remains 0. // AV1 indexes this table from Intra through Alternate. Golden is -1; Backward remains 0.
this.ReferenceDeltas = [1, 0, 0, 0, -1, 0, -1, -1]; this.referenceDeltas[0] = 1;
this.ModeDeltas = [0, 0]; this.referenceDeltas[4] = -1;
this.referenceDeltas[6] = -1;
this.referenceDeltas[7] = -1;
} }
/// <summary> /// <summary>
/// Gets or sets the horizontal and vertical luma filter levels. /// Gets the horizontal and vertical luma filter levels.
/// </summary> /// </summary>
public int[] FilterLevel { get; set; } = new int[2]; public Span<int> FilterLevel => this.filterLevel[..2];
/// <summary> /// <summary>
/// Gets or sets the U-plane filter level. /// Gets or sets the U-plane filter level.
@ -51,10 +68,10 @@ internal class ObuLoopFilterParameters
/// <summary> /// <summary>
/// Gets the filter-level deltas for the AV1 reference-frame categories. /// Gets the filter-level deltas for the AV1 reference-frame categories.
/// </summary> /// </summary>
public int[] ReferenceDeltas { get; } public Span<int> ReferenceDeltas => this.referenceDeltas;
/// <summary> /// <summary>
/// Gets the filter-level deltas for the AV1 prediction modes. /// Gets the filter-level deltas for the AV1 prediction modes.
/// </summary> /// </summary>
public int[] ModeDeltas { get; } public Span<int> ModeDeltas => this.modeDeltas[..2];
} }

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

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Contains the loop-restoration type and unit size for one color plane. /// Contains the loop-restoration type and unit size for one color plane.
/// </summary> /// </summary>
internal class ObuLoopRestorationItem internal struct ObuLoopRestorationItem
{ {
/// <summary> /// <summary>
/// Gets or sets the restoration-unit size, in samples. /// Gets or sets the restoration-unit size, in samples.
@ -16,5 +16,5 @@ internal class ObuLoopRestorationItem
/// <summary> /// <summary>
/// Gets or sets the restoration filter type. /// Gets or sets the restoration filter type.
/// </summary> /// </summary>
public ObuRestorationType Type { get; set; } = ObuRestorationType.None; public ObuRestorationType Type { get; set; }
} }

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

@ -6,20 +6,12 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Contains the AV1 loop-restoration configuration for a frame. /// Contains the AV1 loop-restoration configuration for a frame.
/// </summary> /// </summary>
internal class ObuLoopRestorationParameters internal sealed class ObuLoopRestorationParameters
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ObuLoopRestorationParameters"/> class. /// Stores the fixed three plane configurations without an outer array or per-plane object allocation.
/// </summary> /// </summary>
public ObuLoopRestorationParameters() private InlineArray4<ObuLoopRestorationItem> items;
{
// AV1 addresses restoration state by plane, so all three plane entries must exist even
// when the active color configuration uses fewer planes.
this.Items = new ObuLoopRestorationItem[3];
this.Items[0] = new();
this.Items[1] = new();
this.Items[2] = new();
}
/// <summary> /// <summary>
/// Gets or sets a value indicating whether any plane uses loop restoration. /// Gets or sets a value indicating whether any plane uses loop restoration.
@ -34,7 +26,7 @@ internal class ObuLoopRestorationParameters
/// <summary> /// <summary>
/// Gets the loop-restoration configuration for each plane. /// Gets the loop-restoration configuration for each plane.
/// </summary> /// </summary>
public ObuLoopRestorationItem[] Items { get; } public Span<ObuLoopRestorationItem> Items => this.items[..3];
/// <summary> /// <summary>
/// Gets or sets the luma restoration-unit size shift. /// Gets or sets the luma restoration-unit size shift.

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

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Contains the sequence-level constraints for an AV1 operating point. /// Contains the sequence-level constraints for an AV1 operating point.
/// </summary> /// </summary>
internal class ObuOperatingPoint internal sealed class ObuOperatingPoint
{ {
/// <summary> /// <summary>
/// Gets or sets the operating-point index. /// Gets or sets the operating-point index.

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

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Contains the sequence-level order-hint and dependent prediction-tool settings. /// Contains the sequence-level order-hint and dependent prediction-tool settings.
/// </summary> /// </summary>
internal class ObuOrderHintInfo internal sealed class ObuOrderHintInfo
{ {
/// <summary> /// <summary>
/// Gets or sets a value indicating whether order hints are enabled. /// Gets or sets a value indicating whether order hints are enabled.

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

@ -6,17 +6,37 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Contains the AV1 quantization parameters for a frame. /// Contains the AV1 quantization parameters for a frame.
/// </summary> /// </summary>
internal class ObuQuantizationParameters 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> /// <summary>
/// Gets or sets the base quantizer index. /// Gets or sets the base quantizer index.
/// </summary> /// </summary>
public int BaseQIndex { get; set; } public int BaseQIndex { get; set; }
/// <summary> /// <summary>
/// Gets or sets the effective quantizer index for each segment. /// Gets the mutable effective quantizer indices for each segment.
/// </summary> /// </summary>
public int[] QIndex { get; set; } = new int[Av1Constants.MaxSegmentCount]; public Span<int> QIndex => this.qIndex;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether quantization matrices are enabled. /// Gets or sets a value indicating whether quantization matrices are enabled.
@ -24,19 +44,19 @@ internal class ObuQuantizationParameters
public bool IsUsingQMatrix { get; set; } public bool IsUsingQMatrix { get; set; }
/// <summary> /// <summary>
/// Gets or sets the DC quantizer-index deltas for the Y, U, and V planes. /// Gets the DC quantizer-index deltas for the Y, U, and V planes.
/// </summary> /// </summary>
public int[] DeltaQDc { get; set; } = new int[3]; public Span<int> DeltaQDc => this.deltaQDc[..3];
/// <summary> /// <summary>
/// Gets or sets the AC quantizer-index deltas for the Y, U, and V planes. /// Gets the AC quantizer-index deltas for the Y, U, and V planes.
/// </summary> /// </summary>
public int[] DeltaQAc { get; set; } = new int[3]; public Span<int> DeltaQAc => this.deltaQAc[..3];
/// <summary> /// <summary>
/// Gets or sets the quantization-matrix level for the Y, U, and V planes. /// Gets the quantization-matrix level for the Y, U, and V planes.
/// </summary> /// </summary>
public int[] QMatrix { get; set; } = new int[3]; public Span<int> QMatrix => this.qMatrix[..3];
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the U and V planes use separate quantizer deltas. /// Gets or sets a value indicating whether the U and V planes use separate quantizer deltas.

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

@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Parses AV1 open bitstream units and supplies decoded tile payloads to an AV1 tile reader. /// Parses AV1 open bitstream units and supplies decoded tile payloads to an AV1 tile reader.
/// </summary> /// </summary>
internal class ObuReader internal sealed class ObuReader
{ {
/// <summary> /// <summary>
/// The number of bits used to address one of AV1's eight reference-map slots. /// The number of bits used to address one of AV1's eight reference-map slots.
@ -80,11 +80,6 @@ internal class ObuReader
/// </summary> /// </summary>
private ObuFrameReferenceState frameReferenceState; private ObuFrameReferenceState frameReferenceState;
/// <summary>
/// The tile reader created for the current coded frame.
/// </summary>
private IAv1TileReader? decoder;
/// <summary> /// <summary>
/// The temporal- and spatial-layer mask for the selected operating point. /// The temporal- and spatial-layer mask for the selected operating point.
/// </summary> /// </summary>
@ -119,6 +114,18 @@ internal class ObuReader
this.referenceFrames = referenceFrames; this.referenceFrames = referenceFrames;
} }
/// <summary>
/// Supplies a tile reader while preserving distinct fixed-reader and factory call contracts.
/// </summary>
private interface ITileReaderProvider
{
/// <summary>
/// Gets the tile reader for the current frame.
/// </summary>
/// <returns>The tile reader.</returns>
IAv1TileReader Get();
}
/// <summary> /// <summary>
/// Gets or sets the most recently parsed sequence header. /// Gets or sets the most recently parsed sequence header.
/// </summary> /// </summary>
@ -129,6 +136,39 @@ internal class ObuReader
/// </summary> /// </summary>
public ObuFrameHeader? FrameHeader { get; set; } public ObuFrameHeader? FrameHeader { get; set; }
/// <summary>
/// Gets content light-level metadata parsed from the current AV1 session.
/// </summary>
public HeifContentLightLevel? ContentLightLevel { get; private set; }
/// <summary>
/// Gets mastering-display color-volume metadata parsed from the current AV1 session.
/// </summary>
public HeifMasteringDisplayColorVolume? MasteringDisplayColorVolume { get; private set; }
/// <summary>
/// Gets the sequence header established before frame-dependent syntax is read.
/// </summary>
public ObuSequenceHeader CurrentSequenceHeader =>
this.SequenceHeader
?? throw new InvalidImageContentException("AV1 frame syntax appeared before a sequence header.");
/// <summary>
/// Gets the frame header established before frame-dependent syntax is read.
/// </summary>
public ObuFrameHeader CurrentFrameHeader =>
this.FrameHeader
?? throw new InvalidImageContentException("AV1 tile syntax appeared before a frame header.");
/// <summary>
/// Clears metadata retained from the preceding bounded AV1 image payload.
/// </summary>
public void ResetMetadata()
{
this.ContentLightLevel = null;
this.MasteringDisplayColorVolume = null;
}
/// <summary> /// <summary>
/// Parses every open bitstream unit in one bounded AV1 payload. /// Parses every open bitstream unit in one bounded AV1 payload.
/// </summary> /// </summary>
@ -137,6 +177,40 @@ internal class ObuReader
/// <param name="creator">Creates one tile reader when the first tile payload of each coded frame is encountered.</param> /// <param name="creator">Creates one tile reader when the first tile payload of each coded frame is encountered.</param>
/// <param name="isAnnexB">A value indicating whether each OBU is prefixed by an Annex B length field.</param> /// <param name="isAnnexB">A value indicating whether each OBU is prefixed by an Annex B length field.</param>
public void ReadAll(ref Av1BitStreamReader reader, int dataSize, Func<IAv1TileReader> creator, bool isAnnexB = false) public void ReadAll(ref Av1BitStreamReader reader, int dataSize, Func<IAv1TileReader> creator, bool isAnnexB = false)
{
ArgumentNullException.ThrowIfNull(creator);
this.ReadAll(ref reader, dataSize, new TileReaderFactoryProvider(creator), isAnnexB);
}
/// <summary>
/// Parses every open bitstream unit in one bounded AV1 payload using one existing tile reader.
/// </summary>
/// <param name="reader">The reader positioned at the first OBU.</param>
/// <param name="dataSize">The number of bytes available for the bounded payload.</param>
/// <param name="tileReader">The tile reader used for each coded frame in the payload.</param>
/// <param name="isAnnexB">A value indicating whether each OBU is prefixed by an Annex B length field.</param>
public void ReadAll(ref Av1BitStreamReader reader, int dataSize, IAv1TileReader tileReader, bool isAnnexB = false)
{
ArgumentNullException.ThrowIfNull(tileReader);
this.ReadAll(ref reader, dataSize, new FixedTileReaderProvider(tileReader), isAnnexB);
}
/// <summary>
/// Parses every open bitstream unit in one bounded AV1 payload.
/// </summary>
/// <param name="reader">The reader positioned at the first OBU.</param>
/// <param name="dataSize">The number of bytes available for the bounded payload.</param>
/// <typeparam name="TTileReaderProvider">The non-allocating tile-reader source used by this payload.</typeparam>
/// <param name="tileReaderProvider">Provides one tile reader for each coded frame.</param>
/// <param name="isAnnexB">A value indicating whether each OBU is prefixed by an Annex B length field.</param>
private void ReadAll<TTileReaderProvider>(
ref Av1BitStreamReader reader,
int dataSize,
TTileReaderProvider tileReaderProvider,
bool isAnnexB)
where TTileReaderProvider : struct, ITileReaderProvider
{ {
bool completed = false; bool completed = false;
@ -151,6 +225,7 @@ internal class ObuReader
bool seenFrameHeader = false; bool seenFrameHeader = false;
int nextTileStart = 0; int nextTileStart = 0;
Span<byte> primaryFrameHeaderPayload = default; Span<byte> primaryFrameHeaderPayload = default;
IAv1TileReader? activeDecoder = null;
while (dataSize > 0) while (dataSize > 0)
{ {
@ -220,7 +295,7 @@ internal class ObuReader
} }
Av1BitStreamReader payloadReader = new(obuPayload); Av1BitStreamReader payloadReader = new(obuPayload);
bool frameDecodingFinished = false; IAv1TileReader? decoderToComplete = null;
int decodedPayloadSize; int decodedPayloadSize;
switch (header.Type) switch (header.Type)
@ -276,8 +351,8 @@ internal class ObuReader
if (primaryFrameHeader.ShowExistingFrame) if (primaryFrameHeader.ShowExistingFrame)
{ {
// This header completes by selecting retained samples; no tile group belongs to it. // This header completes by selecting retained samples; no tile group belongs to it.
this.decoder ??= creator(); activeDecoder ??= tileReaderProvider.Get();
frameDecodingFinished = true; decoderToComplete = activeDecoder;
} }
break; break;
@ -336,11 +411,16 @@ internal class ObuReader
throw new InvalidImageContentException("An AV1 tile group appears before its frame header."); throw new InvalidImageContentException("An AV1 tile group appears before its frame header.");
} }
this.decoder ??= creator(); activeDecoder ??= tileReaderProvider.Get();
// A combined frame OBU reaches this label after its frame-header portion has // A combined frame OBU reaches this label after its frame-header portion has
// been consumed, leaving the same tile-group syntax as a standalone tile OBU. // been consumed, leaving the same tile-group syntax as a standalone tile OBU.
this.ReadTileGroup(ref payloadReader, this.decoder, header, ref nextTileStart, out frameDecodingFinished); this.ReadTileGroup(ref payloadReader, activeDecoder, header, ref nextTileStart, out bool frameDecodingFinished);
if (frameDecodingFinished)
{
decoderToComplete = activeDecoder;
}
decodedPayloadSize = Av1Math.DivideBy8Floor(payloadReader.BitPosition); decodedPayloadSize = Av1Math.DivideBy8Floor(payloadReader.BitPosition);
break; break;
case ObuType.TemporalDelimiter: case ObuType.TemporalDelimiter:
@ -353,6 +433,13 @@ internal class ObuReader
// zero bytes between the empty syntax and the declared payload boundary, matching the reference decoder. // zero bytes between the empty syntax and the declared payload boundary, matching the reference decoder.
decodedPayloadSize = 0; decodedPayloadSize = 0;
break; break;
case ObuType.Metadata:
decodedPayloadSize = this.ReadMetadata(obuPayload);
break;
case ObuType.TileList:
// Tile-list OBUs require AV1 large-scale tile mode, which this decoder does not implement.
// Rejecting the syntax avoids silently returning a partial reconstruction.
throw new InvalidImageContentException("AV1 tile-list OBUs are not supported.");
case ObuType.Padding: case ObuType.Padding:
int lastNonzeroIndex = obuPayload.Length - 1; int lastNonzeroIndex = obuPayload.Length - 1;
while (lastNonzeroIndex >= 0 && obuPayload[lastNonzeroIndex] == 0) while (lastNonzeroIndex >= 0 && obuPayload[lastNonzeroIndex] == 0)
@ -375,9 +462,9 @@ internal class ObuReader
decodedPayloadSize = payloadSize; decodedPayloadSize = payloadSize;
break; break;
default: default:
// Metadata, tile-list, and reserved OBUs do not contribute to this still-image reconstruction // Reserved OBUs do not contribute to this still-image reconstruction pass. Their declared payload has
// pass. Their declared payload has already been skipped by the parent reader. the reference decoder rejects a // already been skipped by the parent reader. The reference decoder rejects a nonempty unrecognized
// nonempty unrecognized payload that contains only zeros because it has no trailing one bit. // payload that contains only zeros because it has no trailing one bit.
if (payloadSize > 0) if (payloadSize > 0)
{ {
int ignoredLastNonzeroIndex = payloadSize - 1; int ignoredLastNonzeroIndex = payloadSize - 1;
@ -406,20 +493,23 @@ internal class ObuReader
} }
} }
if (frameDecodingFinished) if (decoderToComplete is not null)
{ {
// Complete reconstruction and reference-buffer ownership before publishing the matching syntax // Complete reconstruction and reference-buffer ownership before publishing the matching syntax
// state. Any decoder failure leaves the preceding session snapshot intact for deterministic cleanup. // state. Any decoder failure leaves the preceding session snapshot intact for deterministic cleanup.
this.decoder!.CompleteFrame(); decoderToComplete.CompleteFrame();
this.frameReferenceState.CompleteFrame(this.FrameHeader!, this.SequenceHeader!.IsFrameIdNumbersPresent); this.frameReferenceState.CompleteFrame(
this.decoder = null; this.CurrentFrameHeader,
this.CurrentSequenceHeader.IsFrameIdNumbersPresent);
activeDecoder = null;
seenFrameHeader = false; seenFrameHeader = false;
nextTileStart = 0; nextTileStart = 0;
primaryFrameHeaderPayload = default; primaryFrameHeaderPayload = default;
} }
} }
if (seenFrameHeader || this.decoder is not null) if (seenFrameHeader || activeDecoder is not null)
{ {
throw new InvalidImageContentException("The AV1 payload ends before the current coded frame is complete."); throw new InvalidImageContentException("The AV1 payload ends before the current coded frame is complete.");
} }
@ -450,9 +540,9 @@ internal class ObuReader
/// </summary> /// </summary>
public void Reset() public void Reset()
{ {
this.decoder = null;
this.SequenceHeader = null; this.SequenceHeader = null;
this.FrameHeader = null; this.FrameHeader = null;
this.ResetMetadata();
this.currentOperatingPointIdc = 0; this.currentOperatingPointIdc = 0;
this.frameReferenceState.Reset(); this.frameReferenceState.Reset();
this.referenceFrames?.Reset(); this.referenceFrames?.Reset();
@ -570,13 +660,234 @@ internal class ObuReader
} }
} }
/// <summary>
/// Reads and validates one AV1 metadata OBU payload.
/// </summary>
/// <param name="payload">The bounded metadata payload.</param>
/// <returns>The number of payload bytes occupied by decoded syntax.</returns>
private int ReadMetadata(Span<byte> payload)
{
int metadataOffset = 0;
ulong metadataTypeValue = ReadMetadataType(payload, ref metadataOffset);
Span<byte> metadataPayload = payload[metadataOffset..];
if (metadataTypeValue == (ulong)ObuMetadataType.Reserved
|| metadataTypeValue > (ulong)ObuMetadataType.Timecode)
{
// Reserved and private metadata have no syntax the decoder can interpret. libaom still requires their
// opaque payload, including its trailing bit, to contain at least one nonzero byte.
if (FindLastNonzeroByteIndex(metadataPayload) < 0)
{
throw new InvalidImageContentException("The AV1 metadata OBU is missing its trailing one bit.");
}
return payload.Length;
}
ObuMetadataType metadataType = (ObuMetadataType)metadataTypeValue;
if (metadataType is ObuMetadataType.HdrCll or ObuMetadataType.HdrMdcv)
{
Av1CodecConfiguration.ReadHdrMetadata(
payload,
"AV1 metadata OBU",
out HeifContentLightLevel? contentLightLevel,
out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume);
this.ContentLightLevel = contentLightLevel ?? this.ContentLightLevel;
this.MasteringDisplayColorVolume = masteringDisplayColorVolume ?? this.MasteringDisplayColorVolume;
return payload.Length;
}
if (metadataType == ObuMetadataType.ItutT35)
{
ValidateItutT35Metadata(metadataPayload);
return payload.Length;
}
Av1BitStreamReader metadataReader = new(metadataPayload);
if (metadataType == ObuMetadataType.Scalability)
{
ReadScalabilityMetadata(ref metadataReader);
}
else
{
ReadTimecodeMetadata(ref metadataReader);
}
ReadTrailingBits(ref metadataReader);
return metadataOffset + Av1Math.DivideBy8Floor(metadataReader.BitPosition);
}
/// <summary>
/// Reads the unsigned little-endian base-128 metadata type.
/// </summary>
/// <param name="payload">The bounded metadata payload.</param>
/// <param name="offset">The byte offset, advanced past the metadata type.</param>
/// <returns>The decoded metadata type.</returns>
private static ulong ReadMetadataType(ReadOnlySpan<byte> payload, ref int offset)
{
ulong value = 0;
for (int i = 0; i < 8; i++)
{
if ((uint)offset >= (uint)payload.Length)
{
throw new InvalidImageContentException("The AV1 metadata type is truncated.");
}
byte current = payload[offset++];
value |= (ulong)(current & 0x7F) << (i * 7);
if ((current & 0x80) == 0)
{
return value;
}
}
throw new InvalidImageContentException("The AV1 metadata type exceeds the permitted LEB128 length.");
}
/// <summary>
/// Validates byte-aligned ITU-T T.35 metadata syntax and trailing bits.
/// </summary>
/// <param name="payload">The metadata bytes following the metadata type.</param>
private static void ValidateItutT35Metadata(ReadOnlySpan<byte> payload)
{
if (payload.IsEmpty)
{
throw new InvalidImageContentException("The AV1 ITU-T T.35 country code is missing.");
}
int countryCodeSize = payload[0] == 0xFF ? 2 : 1;
if (payload.Length < countryCodeSize)
{
throw new InvalidImageContentException("The AV1 ITU-T T.35 country-code extension byte is missing.");
}
int lastNonzeroIndex = FindLastNonzeroByteIndex(payload);
if (lastNonzeroIndex < countryCodeSize || payload[lastNonzeroIndex] != 0x80)
{
throw new InvalidImageContentException("The AV1 ITU-T T.35 metadata has invalid trailing bits.");
}
}
/// <summary>
/// Reads scalability metadata syntax so its bounded payload and trailing bits can be validated.
/// </summary>
/// <param name="reader">The metadata payload reader.</param>
private static void ReadScalabilityMetadata(ref Av1BitStreamReader reader)
{
const uint scalabilityStructureMode = 14;
uint scalabilityMode = reader.ReadLiteral(8);
if (scalabilityMode != scalabilityStructureMode)
{
return;
}
int spatialLayerCount = (int)reader.ReadLiteral(2) + 1;
bool hasSpatialLayerDimensions = reader.ReadBoolean();
bool hasSpatialLayerDescriptions = reader.ReadBoolean();
bool hasTemporalGroupDescriptions = reader.ReadBoolean();
// AV1 reserves these three bits and requires decoders to consume and ignore them.
_ = reader.ReadLiteral(3);
if (hasSpatialLayerDimensions)
{
for (int i = 0; i < spatialLayerCount; i++)
{
_ = reader.ReadLiteral(16);
_ = reader.ReadLiteral(16);
}
}
if (hasSpatialLayerDescriptions)
{
for (int i = 0; i < spatialLayerCount; i++)
{
_ = reader.ReadLiteral(8);
}
}
if (hasTemporalGroupDescriptions)
{
int temporalGroupSize = (int)reader.ReadLiteral(8);
for (int i = 0; i < temporalGroupSize; i++)
{
_ = reader.ReadLiteral(3);
_ = reader.ReadBoolean();
_ = reader.ReadBoolean();
int referenceCount = (int)reader.ReadLiteral(3);
for (int j = 0; j < referenceCount; j++)
{
_ = reader.ReadLiteral(8);
}
}
}
}
/// <summary>
/// Reads timecode metadata syntax so its bounded payload and trailing bits can be validated.
/// </summary>
/// <param name="reader">The metadata payload reader.</param>
private static void ReadTimecodeMetadata(ref Av1BitStreamReader reader)
{
_ = reader.ReadLiteral(5);
bool hasFullTimestamp = reader.ReadBoolean();
_ = reader.ReadBoolean();
_ = reader.ReadBoolean();
_ = reader.ReadLiteral(9);
if (hasFullTimestamp)
{
_ = reader.ReadLiteral(6);
_ = reader.ReadLiteral(6);
_ = reader.ReadLiteral(5);
}
else if (reader.ReadBoolean())
{
_ = reader.ReadLiteral(6);
if (reader.ReadBoolean())
{
_ = reader.ReadLiteral(6);
if (reader.ReadBoolean())
{
_ = reader.ReadLiteral(5);
}
}
}
int timeOffsetLength = (int)reader.ReadLiteral(5);
if (timeOffsetLength > 0)
{
_ = reader.ReadLiteral(timeOffsetLength);
}
}
/// <summary>
/// Finds the final nonzero byte in one bounded payload.
/// </summary>
/// <param name="payload">The payload to inspect.</param>
/// <returns>The final nonzero byte index, or <c>-1</c> when every byte is zero.</returns>
private static int FindLastNonzeroByteIndex(ReadOnlySpan<byte> payload)
{
for (int i = payload.Length - 1; i >= 0; i--)
{
if (payload[i] != 0)
{
return i;
}
}
return -1;
}
/// <summary> /// <summary>
/// Computes the mode-information dimensions and stride for the current frame. /// Computes the mode-information dimensions and stride for the current frame.
/// </summary> /// </summary>
/// <param name="sequenceHeader">The sequence header defining the maximum frame geometry and superblock size.</param> /// <param name="sequenceHeader">The sequence header defining the maximum frame geometry and superblock size.</param>
private void ComputeImageSize(ObuSequenceHeader sequenceHeader) private void ComputeImageSize(ObuSequenceHeader sequenceHeader)
{ {
ObuFrameHeader frameHeader = this.FrameHeader!; ObuFrameHeader frameHeader = this.CurrentFrameHeader;
frameHeader.ModeInfoColumnCount = 2 * ((frameHeader.FrameSize.FrameWidth + 7) >> 3); frameHeader.ModeInfoColumnCount = 2 * ((frameHeader.FrameSize.FrameWidth + 7) >> 3);
frameHeader.ModeInfoRowCount = 2 * ((frameHeader.FrameSize.FrameHeight + 7) >> 3); frameHeader.ModeInfoRowCount = 2 * ((frameHeader.FrameSize.FrameHeight + 7) >> 3);
frameHeader.ModeInfoStride = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, Av1Constants.MaxSuperBlockSizeLog2) >> Av1Constants.ModeInfoSizeLog2; frameHeader.ModeInfoStride = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, Av1Constants.MaxSuperBlockSizeLog2) >> Av1Constants.ModeInfoSizeLog2;
@ -609,7 +920,6 @@ internal class ObuReader
sequenceHeader.TimingInfo = null; sequenceHeader.TimingInfo = null;
sequenceHeader.DecoderModelInfoPresentFlag = false; sequenceHeader.DecoderModelInfoPresentFlag = false;
sequenceHeader.InitialDisplayDelayPresentFlag = false; sequenceHeader.InitialDisplayDelayPresentFlag = false;
sequenceHeader.OperatingPoint = new ObuOperatingPoint[1];
ObuOperatingPoint operatingPoint = new(); ObuOperatingPoint operatingPoint = new();
sequenceHeader.OperatingPoint[0] = operatingPoint; sequenceHeader.OperatingPoint[0] = operatingPoint;
operatingPoint.OperatorIndex = 0; operatingPoint.OperatorIndex = 0;
@ -641,8 +951,12 @@ internal class ObuReader
} }
sequenceHeader.InitialDisplayDelayPresentFlag = reader.ReadBoolean(); sequenceHeader.InitialDisplayDelayPresentFlag = reader.ReadBoolean();
uint operatingPointsCnt = reader.ReadLiteral(5) + 1; int operatingPointsCnt = (int)reader.ReadLiteral(5) + 1;
sequenceHeader.OperatingPoint = new ObuOperatingPoint[operatingPointsCnt]; if (sequenceHeader.OperatingPoint.Length != operatingPointsCnt)
{
sequenceHeader.OperatingPoint = new ObuOperatingPoint[operatingPointsCnt];
}
for (int i = 0; i < operatingPointsCnt; i++) for (int i = 0; i < operatingPointsCnt; i++)
{ {
sequenceHeader.OperatingPoint[i] = new ObuOperatingPoint sequenceHeader.OperatingPoint[i] = new ObuOperatingPoint
@ -671,7 +985,8 @@ internal class ObuReader
{ {
// Operating-point delays affect scheduling rather than still-image reconstruction, but their // Operating-point delays affect scheduling rather than still-image reconstruction, but their
// syntax must be consumed so the following image dimensions remain bit aligned. // syntax must be consumed so the following image dimensions remain bit aligned.
ReadOperatingParametersInfo(ref reader, (int)sequenceHeader.DecoderModelInfo!.BufferDelayLength); ObuDecoderModelInfo decoderModelInfo = sequenceHeader.GetDecoderModelInfo();
ReadOperatingParametersInfo(ref reader, (int)decoderModelInfo.BufferDelayLength);
} }
} }
else else
@ -991,8 +1306,8 @@ internal class ObuReader
/// <param name="reader">The reader positioned at the super-resolution syntax.</param> /// <param name="reader">The reader positioned at the super-resolution syntax.</param>
private void ReadSuperResolutionParameters(ref Av1BitStreamReader reader) private void ReadSuperResolutionParameters(ref Av1BitStreamReader reader)
{ {
ObuSequenceHeader sequenceHeader = this.SequenceHeader!; ObuSequenceHeader sequenceHeader = this.CurrentSequenceHeader;
ObuFrameHeader frameHeader = this.FrameHeader!; ObuFrameHeader frameHeader = this.CurrentFrameHeader;
bool useSuperResolution = false; bool useSuperResolution = false;
if (sequenceHeader.EnableSuperResolution) if (sequenceHeader.EnableSuperResolution)
@ -1034,7 +1349,7 @@ internal class ObuReader
/// <param name="reader">The reader positioned at the render-size syntax.</param> /// <param name="reader">The reader positioned at the render-size syntax.</param>
private void ReadRenderSize(ref Av1BitStreamReader reader) private void ReadRenderSize(ref Av1BitStreamReader reader)
{ {
ObuFrameHeader frameHeader = this.FrameHeader!; ObuFrameHeader frameHeader = this.CurrentFrameHeader;
bool renderSizeAndFrameSizeDifferent = reader.ReadBoolean(); bool renderSizeAndFrameSizeDifferent = reader.ReadBoolean();
if (renderSizeAndFrameSizeDifferent) if (renderSizeAndFrameSizeDifferent)
@ -1058,8 +1373,8 @@ internal class ObuReader
/// <param name="frameSizeOverrideFlag">A value indicating whether dimensions are signaled instead of inherited from the sequence maximum.</param> /// <param name="frameSizeOverrideFlag">A value indicating whether dimensions are signaled instead of inherited from the sequence maximum.</param>
private void ReadFrameSize(ref Av1BitStreamReader reader, bool frameSizeOverrideFlag) private void ReadFrameSize(ref Av1BitStreamReader reader, bool frameSizeOverrideFlag)
{ {
ObuSequenceHeader sequenceHeader = this.SequenceHeader!; ObuSequenceHeader sequenceHeader = this.CurrentSequenceHeader;
ObuFrameHeader frameHeader = this.FrameHeader!; ObuFrameHeader frameHeader = this.CurrentFrameHeader;
if (frameSizeOverrideFlag) if (frameSizeOverrideFlag)
{ {
@ -1092,8 +1407,8 @@ internal class ObuReader
/// <param name="referenceFrames">The retained reconstructed frames selected by the current reference mapping.</param> /// <param name="referenceFrames">The retained reconstructed frames selected by the current reference mapping.</param>
private void ReadFrameSizeWithReferences(ref Av1BitStreamReader reader, Av1ReferenceFrameStore referenceFrames) private void ReadFrameSizeWithReferences(ref Av1BitStreamReader reader, Av1ReferenceFrameStore referenceFrames)
{ {
ObuSequenceHeader sequenceHeader = this.SequenceHeader!; ObuSequenceHeader sequenceHeader = this.CurrentSequenceHeader;
ObuFrameHeader frameHeader = this.FrameHeader!; ObuFrameHeader frameHeader = this.CurrentFrameHeader;
ObuFrameSize frameSize = frameHeader.FrameSize; ObuFrameSize frameSize = frameHeader.FrameSize;
Span<uint> referenceFrameIndices = frameHeader.GetReferenceFrameIndices(); Span<uint> referenceFrameIndices = frameHeader.GetReferenceFrameIndices();
bool foundReference = false; bool foundReference = false;
@ -1107,7 +1422,7 @@ internal class ObuReader
continue; continue;
} }
Av1ReferenceFrame referenceFrame = referenceFrames.Resolve((int)referenceFrameIndices[reference])!; Av1ReferenceFrame referenceFrame = referenceFrames.ResolveRequired((int)referenceFrameIndices[reference]);
ObuFrameSize referenceSize = referenceFrame.FrameHeader.FrameSize; ObuFrameSize referenceSize = referenceFrame.FrameHeader.FrameSize;
// AV1 5.9.7 inherits the reference buffer's visible post-super-resolution dimensions, corresponding to // AV1 5.9.7 inherits the reference buffer's visible post-super-resolution dimensions, corresponding to
@ -1136,7 +1451,7 @@ internal class ObuReader
for (int reference = 0; reference < Av1Constants.ReferencesPerFrame; reference++) for (int reference = 0; reference < Av1Constants.ReferencesPerFrame; reference++)
{ {
Av1ReferenceFrame referenceFrame = referenceFrames.Resolve((int)referenceFrameIndices[reference])!; Av1ReferenceFrame referenceFrame = referenceFrames.ResolveRequired((int)referenceFrameIndices[reference]);
int referenceWidth = referenceFrame.FrameBuffer.Width; int referenceWidth = referenceFrame.FrameBuffer.Width;
int referenceHeight = referenceFrame.FrameBuffer.Height; int referenceHeight = referenceFrame.FrameBuffer.Height;
@ -1210,6 +1525,9 @@ internal class ObuReader
tileInfo.MaxLog2TileRowCount = (int)Av1Math.CeilLog2((uint)Math.Min(superblockRowCount, Av1Constants.MaxTileRowCount)); tileInfo.MaxLog2TileRowCount = (int)Av1Math.CeilLog2((uint)Math.Min(superblockRowCount, Av1Constants.MaxTileRowCount));
tileInfo.MinLog2TileCount = Math.Max(tileInfo.MinLog2TileColumnCount, TileLog2(maxTileAreaOfSuperBlock, superblockColumnCount * superblockRowCount)); tileInfo.MinLog2TileCount = Math.Max(tileInfo.MinLog2TileColumnCount, TileLog2(maxTileAreaOfSuperBlock, superblockColumnCount * superblockRowCount));
tileInfo.HasUniformTileSpacing = reader.ReadBoolean(); tileInfo.HasUniformTileSpacing = reader.ReadBoolean();
// Boundary storage is bounded by AV1's active tile limits. Sequence-sized arrays would retain
// thousands of unused entries on wide frames even though AV1 permits at most 64 rows or columns.
if (tileInfo.HasUniformTileSpacing) if (tileInfo.HasUniformTileSpacing)
{ {
tileInfo.TileColumnCountLog2 = tileInfo.MinLog2TileColumnCount; tileInfo.TileColumnCountLog2 = tileInfo.MinLog2TileColumnCount;
@ -1228,7 +1546,6 @@ internal class ObuReader
int tileWidthSuperblock = Av1Math.DivideLog2Ceiling(superblockColumnCount, tileInfo.TileColumnCountLog2); int tileWidthSuperblock = Av1Math.DivideLog2Ceiling(superblockColumnCount, tileInfo.TileColumnCountLog2);
DebugGuard.MustBeLessThanOrEqualTo(tileWidthSuperblock, tileInfo.MaxTileWidthSuperblock, nameof(tileWidthSuperblock)); DebugGuard.MustBeLessThanOrEqualTo(tileWidthSuperblock, tileInfo.MaxTileWidthSuperblock, nameof(tileWidthSuperblock));
int i = 0; int i = 0;
tileInfo.TileColumnStartModeInfo = new int[superblockColumnCount + 1];
for (int startSuperblock = 0; startSuperblock < superblockColumnCount; startSuperblock += tileWidthSuperblock) for (int startSuperblock = 0; startSuperblock < superblockColumnCount; startSuperblock += tileWidthSuperblock)
{ {
tileInfo.TileColumnStartModeInfo[i] = startSuperblock << superblockShift; tileInfo.TileColumnStartModeInfo[i] = startSuperblock << superblockShift;
@ -1255,7 +1572,6 @@ internal class ObuReader
int tileHeightSuperblock = Av1Math.DivideLog2Ceiling(superblockRowCount, tileInfo.TileRowCountLog2); int tileHeightSuperblock = Av1Math.DivideLog2Ceiling(superblockRowCount, tileInfo.TileRowCountLog2);
DebugGuard.MustBeLessThanOrEqualTo(tileHeightSuperblock, tileInfo.MaxTileHeightSuperblock, nameof(tileHeightSuperblock)); DebugGuard.MustBeLessThanOrEqualTo(tileHeightSuperblock, tileInfo.MaxTileHeightSuperblock, nameof(tileHeightSuperblock));
i = 0; i = 0;
tileInfo.TileRowStartModeInfo = new int[superblockRowCount + 1];
for (int startSuperblock = 0; startSuperblock < superblockRowCount; startSuperblock += tileHeightSuperblock) for (int startSuperblock = 0; startSuperblock < superblockRowCount; startSuperblock += tileHeightSuperblock)
{ {
tileInfo.TileRowStartModeInfo[i] = startSuperblock << superblockShift; tileInfo.TileRowStartModeInfo[i] = startSuperblock << superblockShift;
@ -1272,6 +1588,11 @@ internal class ObuReader
int i = 0; int i = 0;
for (; startSuperBlock < superblockColumnCount; i++) for (; startSuperBlock < superblockColumnCount; i++)
{ {
if (i == Av1Constants.MaxTileColumnCount)
{
throw new InvalidImageContentException("The AV1 frame exceeds the maximum tile-column count.");
}
tileInfo.TileColumnStartModeInfo[i] = startSuperBlock << superblockShift; tileInfo.TileColumnStartModeInfo[i] = startSuperBlock << superblockShift;
uint maxWidth = (uint)Math.Min(superblockColumnCount - startSuperBlock, tileInfo.MaxTileWidthSuperblock); uint maxWidth = (uint)Math.Min(superblockColumnCount - startSuperBlock, tileInfo.MaxTileWidthSuperblock);
uint widthInSuperBlocks = reader.ReadNonSymmetric(maxWidth) + 1; uint widthInSuperBlocks = reader.ReadNonSymmetric(maxWidth) + 1;
@ -1302,6 +1623,11 @@ internal class ObuReader
startSuperBlock = 0; startSuperBlock = 0;
for (i = 0; startSuperBlock < superblockRowCount; i++) for (i = 0; startSuperBlock < superblockRowCount; i++)
{ {
if (i == Av1Constants.MaxTileRowCount)
{
throw new InvalidImageContentException("The AV1 frame exceeds the maximum tile-row count.");
}
tileInfo.TileRowStartModeInfo[i] = startSuperBlock << superblockShift; tileInfo.TileRowStartModeInfo[i] = startSuperBlock << superblockShift;
uint maxHeight = (uint)Math.Min(superblockRowCount - startSuperBlock, tileInfo.MaxTileHeightSuperblock); uint maxHeight = (uint)Math.Min(superblockRowCount - startSuperBlock, tileInfo.MaxTileHeightSuperblock);
uint heightInSuperBlocks = reader.ReadNonSymmetric(maxHeight) + 1; uint heightInSuperBlocks = reader.ReadNonSymmetric(maxHeight) + 1;
@ -1366,8 +1692,8 @@ internal class ObuReader
/// <param name="header">The OBU header identifying the frame's temporal and spatial layers.</param> /// <param name="header">The OBU header identifying the frame's temporal and spatial layers.</param>
private void ReadUncompressedFrameHeader(ref Av1BitStreamReader reader, ObuHeader header) private void ReadUncompressedFrameHeader(ref Av1BitStreamReader reader, ObuHeader header)
{ {
ObuSequenceHeader sequenceHeader = this.SequenceHeader!; ObuSequenceHeader sequenceHeader = this.CurrentSequenceHeader;
ObuFrameHeader frameHeader = this.FrameHeader!; ObuFrameHeader frameHeader = this.CurrentFrameHeader;
Av1ReferenceFrame? primaryReference = null; Av1ReferenceFrame? primaryReference = null;
bool frameSizeOverrideFlag = false; bool frameSizeOverrideFlag = false;
int idLength = sequenceHeader.FrameIdLength; int idLength = sequenceHeader.FrameIdLength;
@ -1395,7 +1721,8 @@ internal class ObuReader
if (sequenceHeader.DecoderModelInfoPresentFlag && sequenceHeader.TimingInfo?.EqualPictureInterval == false) if (sequenceHeader.DecoderModelInfoPresentFlag && sequenceHeader.TimingInfo?.EqualPictureInterval == false)
{ {
// 5.9.31. Temporal point info syntax. // 5.9.31. Temporal point info syntax.
frameHeader.FramePresentationTime = reader.ReadLiteral((int)sequenceHeader!.DecoderModelInfo!.FramePresentationTimeLength); ObuDecoderModelInfo decoderModelInfo = sequenceHeader.GetDecoderModelInfo();
frameHeader.FramePresentationTime = reader.ReadLiteral((int)decoderModelInfo.FramePresentationTimeLength);
} }
if (sequenceHeader.IsFrameIdNumbersPresent) if (sequenceHeader.IsFrameIdNumbersPresent)
@ -1464,7 +1791,8 @@ internal class ObuReader
if (frameHeader.ShowFrame && sequenceHeader.DecoderModelInfoPresentFlag && sequenceHeader.TimingInfo?.EqualPictureInterval == false) if (frameHeader.ShowFrame && sequenceHeader.DecoderModelInfoPresentFlag && sequenceHeader.TimingInfo?.EqualPictureInterval == false)
{ {
// 5.9.31. Temporal point info syntax. // 5.9.31. Temporal point info syntax.
frameHeader.FramePresentationTime = reader.ReadLiteral((int)sequenceHeader!.DecoderModelInfo!.FramePresentationTimeLength); ObuDecoderModelInfo decoderModelInfo = sequenceHeader.GetDecoderModelInfo();
frameHeader.FramePresentationTime = reader.ReadLiteral((int)decoderModelInfo.FramePresentationTimeLength);
} }
if (frameHeader.ShowFrame) if (frameHeader.ShowFrame)
@ -1579,7 +1907,8 @@ internal class ObuReader
bool bufferRemovalTimePresent = reader.ReadBoolean(); bool bufferRemovalTimePresent = reader.ReadBoolean();
if (bufferRemovalTimePresent) if (bufferRemovalTimePresent)
{ {
int bufferRemovalTimeLength = (int)sequenceHeader.DecoderModelInfo!.BufferRemovalTimeLength; ObuDecoderModelInfo decoderModelInfo = sequenceHeader.GetDecoderModelInfo();
int bufferRemovalTimeLength = (int)decoderModelInfo.BufferRemovalTimeLength;
foreach (ObuOperatingPoint operatingPoint in sequenceHeader.OperatingPoint) foreach (ObuOperatingPoint operatingPoint in sequenceHeader.OperatingPoint)
{ {
// A layer-specific OBU carries one removal time only for operating points which select both // A layer-specific OBU carries one removal time only for operating points which select both
@ -1662,7 +1991,7 @@ internal class ObuReader
{ {
// Reference-index parsing validates the resolved slot before publishing it on the header. Retaining // Reference-index parsing validates the resolved slot before publishing it on the header. Retaining
// the owner here keeps every inherited frame state tied to the same normative primary reference. // the owner here keeps every inherited frame state tied to the same normative primary reference.
primaryReference = retainedReferenceFrames.Resolve(frameHeader.PrimaryReferenceSlot.Value)!; primaryReference = retainedReferenceFrames.ResolveRequired(frameHeader.PrimaryReferenceSlot.Value);
} }
if (!frameHeader.ErrorResilientMode && frameSizeOverrideFlag) if (!frameHeader.ErrorResilientMode && frameSizeOverrideFlag)
@ -1700,7 +2029,8 @@ internal class ObuReader
// SetupFrameBufferReferences(sequenceHeader, frameHeader); // SetupFrameBufferReferences(sequenceHeader, frameHeader);
// CheckAddTemporalMotionVectorBuffer(sequenceHeader, frameHeader); // CheckAddTemporalMotionVectorBuffer(sequenceHeader, frameHeader);
// SetupFrameSignBias(sequenceHeader, frameHeader); // Sign bias is derived from retained reference order hints when frame motion state is initialized. It is not
// mutable uncompressed-header state and therefore is not duplicated here.
if (sequenceHeader.IsReducedStillPictureHeader || frameHeader.DisableCdfUpdate) if (sequenceHeader.IsReducedStillPictureHeader || frameHeader.DisableCdfUpdate)
{ {
frameHeader.DisableFrameEndUpdateCdf = true; frameHeader.DisableFrameEndUpdateCdf = true;
@ -1714,14 +2044,15 @@ internal class ObuReader
{ {
// When update flags omit new values, loop-filter deltas inherit from the primary frame. Copying the two // When update flags omit new values, loop-filter deltas inherit from the primary frame. Copying the two
// fixed tables before parsing lets the existing header object retain unchanged entries without aliases. // fixed tables before parsing lets the existing header object retain unchanged entries without aliases.
primaryReference.FrameHeader.LoopFilterParameters.ReferenceDeltas.AsSpan().CopyTo(frameHeader.LoopFilterParameters.ReferenceDeltas); primaryReference.FrameHeader.LoopFilterParameters.ReferenceDeltas.CopyTo(frameHeader.LoopFilterParameters.ReferenceDeltas);
primaryReference.FrameHeader.LoopFilterParameters.ModeDeltas.AsSpan().CopyTo(frameHeader.LoopFilterParameters.ModeDeltas); primaryReference.FrameHeader.LoopFilterParameters.ModeDeltas.CopyTo(frameHeader.LoopFilterParameters.ModeDeltas);
} }
// Entropy defaults depend on base_q_idx, which follows tile information in the header. Av1TileReader therefore // Entropy defaults depend on base_q_idx, which follows tile information in the header. Av1TileReader therefore
// loads either the retained primary snapshot or the selected quantizer-band defaults at the first tile boundary. // loads either the retained primary snapshot or the selected quantizer-band defaults at the first tile boundary.
// GenerateNextReferenceFrameMap(sequenceHeader, frameHeader); // Reference-map refresh remains transactional until reconstruction completes; parsing only records the
// validated refresh flags and selected slots on the frame header.
frameHeader.TilesInfo = ReadTileInfo(ref reader, sequenceHeader, frameHeader); frameHeader.TilesInfo = ReadTileInfo(ref reader, sequenceHeader, frameHeader);
ReadQuantizationParameters(ref reader, sequenceHeader, frameHeader); ReadQuantizationParameters(ref reader, sequenceHeader, frameHeader);
ReadSegmentationParameters(ref reader, frameHeader, primaryReference?.FrameHeader.SegmentationParameters); ReadSegmentationParameters(ref reader, frameHeader, primaryReference?.FrameHeader.SegmentationParameters);
@ -1842,16 +2173,6 @@ internal class ObuReader
} }
} }
/// <summary>
/// Determines whether segmentation and a specific per-segment feature are both enabled.
/// </summary>
/// <param name="segmentationParameters">The frame segmentation state.</param>
/// <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>
private static bool IsSegmentationFeatureActive(ObuSegmentationParameters segmentationParameters, int segmentId, ObuSegmentationLevelFeature feature)
=> segmentationParameters.Enabled && segmentationParameters.IsFeatureActive(segmentId, feature);
/// <summary> /// <summary>
/// Reads an AV1 frame header and removes its byte length from the remaining OBU payload size. /// Reads an AV1 frame header and removes its byte length from the remaining OBU payload size.
/// </summary> /// </summary>
@ -1889,9 +2210,9 @@ internal class ObuReader
ref int nextTileStart, ref int nextTileStart,
out bool isLastTileGroup) out bool isLastTileGroup)
{ {
ObuSequenceHeader sequenceHeader = this.SequenceHeader!; ObuSequenceHeader sequenceHeader = this.CurrentSequenceHeader;
ObuFrameHeader frameHeader = this.FrameHeader!; ObuFrameHeader frameHeader = this.CurrentFrameHeader;
ObuTileGroupHeader tileInfo = this.FrameHeader!.TilesInfo; ObuTileGroupHeader tileInfo = frameHeader.TilesInfo;
int tileCount = tileInfo.TileColumnCount * tileInfo.TileRowCount; int tileCount = tileInfo.TileColumnCount * tileInfo.TileRowCount;
int startBitPosition = reader.BitPosition; int startBitPosition = reader.BitPosition;
bool tileStartAndEndPresentFlag = false; bool tileStartAndEndPresentFlag = false;
@ -1982,8 +2303,6 @@ internal class ObuReader
{ {
return; return;
} }
// TODO: Share doCdef and doLoopRestoration
} }
/// <summary> /// <summary>
@ -2153,7 +2472,7 @@ internal class ObuReader
{ {
int featureValue = 0; int featureValue = 0;
bool featureEnabled = reader.ReadBoolean(); bool featureEnabled = reader.ReadBoolean();
frameHeader.SegmentationParameters.FeatureEnabled[i, j] = featureEnabled; frameHeader.SegmentationParameters.SetFeatureEnabled(i, j, featureEnabled);
int clippedValue = 0; int clippedValue = 0;
if (featureEnabled) if (featureEnabled)
{ {
@ -2171,7 +2490,7 @@ internal class ObuReader
} }
} }
frameHeader.SegmentationParameters.FeatureData[i, j] = clippedValue; frameHeader.SegmentationParameters.SetFeatureData(i, j, clippedValue);
} }
} }
} }
@ -2179,7 +2498,12 @@ internal class ObuReader
{ {
// update_data equal to zero preserves the complete feature mask and values from the primary frame. // update_data equal to zero preserves the complete feature mask and values from the primary frame.
// The current header owns its arrays, so later reference replacement cannot mutate inherited state. // The current header owns its arrays, so later reference replacement cannot mutate inherited state.
frameHeader.SegmentationParameters.CopyFeaturesFrom(primaryParameters!); if (primaryParameters is null)
{
throw new InvalidImageContentException("AV1 segmentation cannot inherit data without a primary reference.");
}
frameHeader.SegmentationParameters.CopyFeaturesFrom(primaryParameters);
} }
} }
else else
@ -2188,8 +2512,8 @@ internal class ObuReader
{ {
for (int j = 0; j < Av1Constants.SegmentationLevelMax; j++) for (int j = 0; j < Av1Constants.SegmentationLevelMax; j++)
{ {
frameHeader.SegmentationParameters.FeatureEnabled[i, j] = false; frameHeader.SegmentationParameters.SetFeatureEnabled(i, j, false);
frameHeader.SegmentationParameters.FeatureData[i, j] = 0; frameHeader.SegmentationParameters.SetFeatureData(i, j, 0);
} }
} }
} }
@ -2200,7 +2524,7 @@ internal class ObuReader
{ {
for (int j = 0; j < Av1Constants.SegmentationLevelMax; j++) for (int j = 0; j < Av1Constants.SegmentationLevelMax; j++)
{ {
if (frameHeader.SegmentationParameters.FeatureEnabled[i, j]) if (frameHeader.SegmentationParameters.IsFeatureActive(i, (ObuSegmentationLevelFeature)j))
{ {
frameHeader.SegmentationParameters.LastActiveSegmentId = i; frameHeader.SegmentationParameters.LastActiveSegmentId = i;
if (j >= (int)ObuSegmentationLevelFeature.ReferenceFrame) if (j >= (int)ObuSegmentationLevelFeature.ReferenceFrame)
@ -2219,7 +2543,7 @@ internal class ObuReader
/// <param name="sequenceHeader">The sequence header defining the active color planes.</param> /// <param name="sequenceHeader">The sequence header defining the active color planes.</param>
private void ReadLoopFilterParameters(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader) private void ReadLoopFilterParameters(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader)
{ {
ObuFrameHeader frameHeader = this.FrameHeader!; ObuFrameHeader frameHeader = this.CurrentFrameHeader;
if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy) if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy)
{ {
return; return;
@ -2400,7 +2724,10 @@ internal class ObuReader
{ {
// primary_ref_frame identifies the preceding frame whose same seven canonical reference roles supply the // primary_ref_frame identifies the preceding frame whose same seven canonical reference roles supply the
// recentering values. Reference-slot validation has already completed before this syntax is reached. // recentering values. Reference-slot validation has already completed before this syntax is reached.
primaryReferenceHeader = this.referenceFrames!.Resolve(primaryReferenceSlot.Value)!.FrameHeader; Av1ReferenceFrameStore referenceFrames = this.referenceFrames
?? throw new InvalidImageContentException("AV1 global motion requires a reconstructed reference map.");
primaryReferenceHeader = referenceFrames.ResolveRequired(primaryReferenceSlot.Value).FrameHeader;
} }
for (int referenceIndex = 0; referenceIndex < Av1Constants.ReferencesPerFrame; referenceIndex++) for (int referenceIndex = 0; referenceIndex < Av1Constants.ReferencesPerFrame; referenceIndex++)
@ -2764,4 +3091,20 @@ internal class ObuReader
return k; return k;
} }
// The generic providers keep the fixed-reader and factory contracts distinct without allocating
// a closure for fixed-reader payloads or admitting an invalid pair of nullable arguments.
private readonly struct TileReaderFactoryProvider(Func<IAv1TileReader> creator) : ITileReaderProvider
{
private readonly Func<IAv1TileReader> creator = creator;
public IAv1TileReader Get() => this.creator();
}
private readonly struct FixedTileReaderProvider(IAv1TileReader tileReader) : ITileReaderProvider
{
private readonly IAv1TileReader tileReader = tileReader;
public IAv1TileReader Get() => this.tileReader;
}
} }

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

@ -6,27 +6,32 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Contains the AV1 segmentation state and per-segment feature values for a frame. /// Contains the AV1 segmentation state and per-segment feature values for a frame.
/// </summary> /// </summary>
internal class ObuSegmentationParameters internal sealed class ObuSegmentationParameters
{ {
/// <summary> /// <summary>
/// Gets or sets the effective quantization-matrix level for each plane and segment. /// Stores the feature-enable flags for each segment and feature without per-frame array allocations.
/// </summary> /// </summary>
public int[][] QMLevel { get; set; } = new int[3][]; private InlineArray8<InlineArray8<bool>> featureEnabled;
/// <summary> /// <summary>
/// Gets or sets the enabled state of every feature for every segment. /// Stores the feature values for each segment and feature without per-frame array allocations.
/// </summary> /// </summary>
public bool[,] FeatureEnabled { get; set; } = new bool[Av1Constants.MaxSegmentCount, Av1Constants.SegmentationLevelMax]; private InlineArray8<InlineArray8<int>> featureData;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether segmentation is enabled for the frame. /// Stores the effective quantization-matrix level for each plane and segment without jagged arrays.
/// </summary> /// </summary>
public bool Enabled { get; set; } 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> /// <summary>
/// Gets or sets the value of every feature for every segment. /// Gets or sets a value indicating whether segmentation is enabled for the frame.
/// </summary> /// </summary>
public int[,] FeatureData { get; set; } = new int[Av1Constants.MaxSegmentCount, Av1Constants.SegmentationLevelMax]; public bool Enabled { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether segment identifiers are decoded before skip-mode decisions. /// Gets or sets a value indicating whether segment identifiers are decoded before skip-mode decisions.
@ -60,7 +65,33 @@ internal class ObuSegmentationParameters
/// <param name="feature">The feature to inspect.</param> /// <param name="feature">The feature to inspect.</param>
/// <returns><see langword="true"/> when the feature is active; otherwise, <see langword="false"/>.</returns> /// <returns><see langword="true"/> when the feature is active; otherwise, <see langword="false"/>.</returns>
public bool IsFeatureActive(int segmentId, ObuSegmentationLevelFeature feature) public bool IsFeatureActive(int segmentId, ObuSegmentationLevelFeature feature)
=> this.FeatureEnabled[segmentId, (int)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> /// <summary>
/// Replaces every feature enable flag and value with state from a primary reference frame. /// Replaces every feature enable flag and value with state from a primary reference frame.
@ -74,8 +105,8 @@ internal class ObuSegmentationParameters
{ {
for (int feature = 0; feature < Av1Constants.SegmentationLevelMax; feature++) for (int feature = 0; feature < Av1Constants.SegmentationLevelMax; feature++)
{ {
this.FeatureEnabled[segment, feature] = source.FeatureEnabled[segment, feature]; this.featureEnabled[segment][feature] = source.featureEnabled[segment][feature];
this.FeatureData[segment, feature] = source.FeatureData[segment, feature]; this.featureData[segment][feature] = source.featureData[segment][feature];
} }
} }
} }

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

@ -6,13 +6,18 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Stores AV1 sequence capabilities, dimensions, timing, and color configuration. /// Stores AV1 sequence capabilities, dimensions, timing, and color configuration.
/// </summary> /// </summary>
internal class ObuSequenceHeader internal sealed class ObuSequenceHeader
{ {
/// <summary> /// <summary>
/// Backs <see cref="Use128x128Superblock"/> while its dependent geometry is updated. /// Backs <see cref="Use128x128Superblock"/> while its dependent geometry is updated.
/// </summary> /// </summary>
private bool use128x128Superblock; 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> /// <summary>
/// Gets or sets a value indicating whether filter-intra prediction is enabled. /// Gets or sets a value indicating whether filter-intra prediction is enabled.
/// </summary> /// </summary>
@ -213,4 +218,12 @@ internal class ObuSequenceHeader
/// Gets or sets the additional frame-identifier bit count signaled by the sequence header. /// Gets or sets the additional frame-identifier bit count signaled by the sequence header.
/// </summary> /// </summary>
public uint AdditionalFrameIdLength { get; set; } 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.");
} }

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

@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Contains the availability and enabled state of AV1 skip mode for a frame. /// Contains the availability and enabled state of AV1 skip mode for a frame.
/// </summary> /// </summary>
internal class ObuSkipModeParameters internal sealed class ObuSkipModeParameters
{ {
/// <summary> /// <summary>
/// Gets a value indicating whether the frame is permitted to use skip mode. /// Gets a value indicating whether the frame is permitted to use skip mode.

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

@ -1,13 +1,18 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Contains the tile layout derived from an AV1 frame header. /// Contains the tile layout derived from an AV1 frame header.
/// </summary> /// </summary>
internal class ObuTileGroupHeader internal sealed class ObuTileGroupHeader
{ {
private InlineTileColumnBoundaryArray tileColumnStartModeInfo;
private InlineTileRowBoundaryArray tileRowStartModeInfo;
/// <summary> /// <summary>
/// Gets or sets the maximum tile width, in superblocks. /// Gets or sets the maximum tile width, in superblocks.
/// </summary> /// </summary>
@ -54,9 +59,9 @@ internal class ObuTileGroupHeader
public int TileColumnCount { get; set; } public int TileColumnCount { get; set; }
/// <summary> /// <summary>
/// Gets or sets the starting superblock column for each tile column. /// Gets the fixed-capacity starting superblock column storage for each tile column.
/// </summary> /// </summary>
public int[] TileColumnStartModeInfo { get; set; } = new int[Av1Constants.MaxTileRowCount + 1]; public Span<int> TileColumnStartModeInfo => this.tileColumnStartModeInfo;
/// <summary> /// <summary>
/// Gets or sets the minimum base-2 logarithm of the tile-row count. /// Gets or sets the minimum base-2 logarithm of the tile-row count.
@ -69,9 +74,9 @@ internal class ObuTileGroupHeader
public int TileRowCountLog2 { get; set; } public int TileRowCountLog2 { get; set; }
/// <summary> /// <summary>
/// Gets or sets the starting superblock row for each tile row. /// Gets the fixed-capacity starting superblock row storage for each tile row.
/// </summary> /// </summary>
public int[] TileRowStartModeInfo { get; set; } = new int[Av1Constants.MaxTileColumnCount + 1]; public Span<int> TileRowStartModeInfo => this.tileRowStartModeInfo;
/// <summary> /// <summary>
/// Gets or sets the number of tile rows. /// Gets or sets the number of tile rows.
@ -87,4 +92,16 @@ internal class ObuTileGroupHeader
/// Gets or sets the number of bytes used to signal each tile size. /// Gets or sets the number of bytes used to signal each tile size.
/// </summary> /// </summary>
public int TileSizeBytes { get; set; } 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;
}
} }

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

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
/// <summary> /// <summary>
/// Contains the timing syntax signaled by an AV1 sequence header. /// Contains the timing syntax signaled by an AV1 sequence header.
/// </summary> /// </summary>
internal class ObuTimingInfo internal sealed class ObuTimingInfo
{ {
/// <summary> /// <summary>
/// Gets or sets NumUnitsInDisplayTick. NumUnitsInDisplayTick is the number of time units of a clock operating at the frequency TimeScale Hz that /// Gets or sets NumUnitsInDisplayTick. NumUnitsInDisplayTick is the number of time units of a clock operating at the frequency TimeScale Hz that

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

@ -629,7 +629,7 @@ internal class ObuWriter
{ {
for (int featureId = 0; featureId < Av1Constants.SegmentationLevelMax; featureId++) for (int featureId = 0; featureId < Av1Constants.SegmentationLevelMax; featureId++)
{ {
bool enabled = segmentation.FeatureEnabled[segmentId, featureId]; bool enabled = segmentation.IsFeatureActive(segmentId, (ObuSegmentationLevelFeature)featureId);
writer.WriteBoolean(enabled); writer.WriteBoolean(enabled);
if (!enabled) if (!enabled)
{ {
@ -637,7 +637,7 @@ internal class ObuWriter
} }
int bitCount = Av1Constants.SegmentationFeatureBits[featureId]; int bitCount = Av1Constants.SegmentationFeatureBits[featureId];
int value = segmentation.FeatureData[segmentId, featureId]; int value = segmentation.GetFeatureData(segmentId, featureId);
if (Av1Constants.SegmentationFeatureSigned[featureId] == 1) if (Av1Constants.SegmentationFeatureSigned[featureId] == 1)
{ {
writer.WriteSignedFromUnsigned(value, bitCount + 1); writer.WriteSignedFromUnsigned(value, bitCount + 1);

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

@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.SuperResolution;
using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
@ -41,7 +42,7 @@ internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable
/// <summary> /// <summary>
/// The retained reconstructed frames addressable by inter prediction. /// The retained reconstructed frames addressable by inter prediction.
/// </summary> /// </summary>
private readonly Av1ReferenceFrameStore? referenceFrames; private readonly Av1ReferenceFrameStore referenceFrames;
/// <summary> /// <summary>
/// The coefficient inverse-quantization stage shared across superblocks. /// The coefficient inverse-quantization stage shared across superblocks.
@ -70,15 +71,15 @@ internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable
/// <param name="frameHeader">The parsed AV1 frame header.</param> /// <param name="frameHeader">The parsed AV1 frame header.</param>
/// <param name="frameInfo">The parsed superblock and block-mode information.</param> /// <param name="frameInfo">The parsed superblock and block-mode information.</param>
/// <param name="frameBuffer">The destination planar sample buffers.</param> /// <param name="frameBuffer">The destination planar sample buffers.</param>
/// <param name="referenceFrames"> /// <param name="referenceFrames">The retained reconstructed frames selected by inter blocks.</param>
/// The retained reconstructed frames selected by inter blocks, or <see langword="null"/> for intra-only reconstruction. /// <param name="paletteColorIndexMaps">The complete decoder-session palette map state.</param>
/// </param>
public Av1FrameDecoder( public Av1FrameDecoder(
ObuSequenceHeader sequenceHeader, ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader, ObuFrameHeader frameHeader,
Av1FrameInfo frameInfo, Av1FrameInfo frameInfo,
Av1FrameBuffer<byte> frameBuffer, Av1FrameBuffer<byte> frameBuffer,
Av1ReferenceFrameStore? referenceFrames = null) Av1ReferenceFrameStore referenceFrames,
Av1TileReader.PaletteColorIndexMaps? paletteColorIndexMaps = null)
{ {
this.sequenceHeader = sequenceHeader; this.sequenceHeader = sequenceHeader;
this.frameHeader = frameHeader; this.frameHeader = frameHeader;
@ -87,20 +88,33 @@ internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable
this.referenceFrames = referenceFrames; this.referenceFrames = referenceFrames;
this.inverseQuantizer = new(sequenceHeader, frameHeader); this.inverseQuantizer = new(sequenceHeader, frameHeader);
this.deQuants = new(sequenceHeader, frameHeader); this.deQuants = new(sequenceHeader, frameHeader);
this.loopFilterContext = new(sequenceHeader); this.loopFilterContext = new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
this.blockDecoder = new( try
this.sequenceHeader, {
this.frameHeader, this.blockDecoder = new(
this.frameBuffer, this.sequenceHeader,
this.loopFilterContext, this.frameHeader,
this.inverseQuantizer, this.frameBuffer,
this.referenceFrames); this.loopFilterContext,
this.inverseQuantizer,
this.referenceFrames,
paletteColorIndexMaps);
}
catch
{
this.loopFilterContext.Dispose();
throw;
}
} }
/// <summary> /// <summary>
/// Releases the pooled block-reconstruction workspaces owned by this decoder. /// Releases the pooled block-reconstruction workspaces owned by this decoder.
/// </summary> /// </summary>
public void Dispose() => this.blockDecoder.Dispose(); public void Dispose()
{
this.blockDecoder.Dispose();
this.loopFilterContext.Dispose();
}
/// <summary> /// <summary>
/// Applies the in-loop frame stages after every superblock has been reconstructed. /// Applies the in-loop frame stages after every superblock has been reconstructed.

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

@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
/// <summary> /// <summary>
/// Applies AV1 constrained directional enhancement filtering to a reconstructed still-image frame. /// Applies AV1 constrained directional enhancement filtering to a reconstructed still-image frame.
/// </summary> /// </summary>
internal class Av1CdefDecoder internal sealed class Av1CdefDecoder
{ {
/// <summary> /// <summary>
/// The width and height of a CDEF unit in 4x4 luma mode-information units. /// The width and height of a CDEF unit in 4x4 luma mode-information units.
@ -30,6 +30,21 @@ internal class Av1CdefDecoder
/// </summary> /// </summary>
private const int MaximumBlocksPerUnit = 8 * 8; 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> /// <summary>
/// The sequence-level superblock, bit-depth, and color configuration. /// The sequence-level superblock, bit-depth, and color configuration.
/// </summary> /// </summary>
@ -97,144 +112,109 @@ internal class Av1CdefDecoder
return; return;
} }
int lumaBlockColumnCount = this.frameHeader.ModeInfoColumnCount >> 1;
int lumaBlockRowCount = this.frameHeader.ModeInfoRowCount >> 1;
int mapLength = lumaBlockColumnCount * lumaBlockRowCount;
MemoryAllocator allocator = this.frameBuffer.MemoryAllocator;
using IMemoryOwner<int> directionOwner = allocator.Allocate<int>(mapLength, AllocationOptions.Clean);
using IMemoryOwner<int> varianceOwner = allocator.Allocate<int>(mapLength, AllocationOptions.Clean);
Span<int> directions = directionOwner.Memory.Span[..mapLength];
Span<int> variances = varianceOwner.Memory.Span[..mapLength];
ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig; ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig;
int planeCount = colorConfig.PlaneCount;
// Luma must be processed first even when its strengths are zero because chroma CDEF Span<int> subsamplingX = stackalloc int[3];
// consumes directions derived from the immutable, deblocked luma source. Span<int> subsamplingY = stackalloc int[3];
for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++) 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; Av1Plane plane = (Av1Plane)planeIndex;
int subsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0; int planeSubsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0;
int subsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0; int planeSubsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0;
this.FilterPlane(plane, subsamplingX, subsamplingY, directions, variances, lumaBlockColumnCount); int planeWidth = this.frameHeader.ModeInfoColumnCount << (Av1Constants.ModeInfoSizeLog2 - planeSubsamplingX);
} int maximumUnitHeight = MaximumUnitPlaneSize >> planeSubsamplingY;
}
subsamplingX[planeIndex] = planeSubsamplingX;
/// <summary> subsamplingY[planeIndex] = planeSubsamplingY;
/// Filters one color plane from an immutable snapshot of its deblocked samples. planeWidths[planeIndex] = planeWidth;
/// </summary> lineBufferOffsets[planeIndex] = lineBufferLength;
/// <param name="plane">The color plane to filter.</param> columnBufferOffsets[planeIndex] = columnBufferLength;
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param> columnBufferLengths[planeIndex] = (maximumUnitHeight + (SourceBorder * 2)) * SourceBorder;
/// <param name="subsamplingY">The vertical chroma subsampling shift.</param> lineBufferLength += planeWidth * SourceBorder * 2;
/// <param name="directions">The frame-wide luma direction map in 8x8 block order.</param> columnBufferLength += columnBufferLengths[planeIndex];
/// <param name="variances">The frame-wide luma directional-variance map in 8x8 block order.</param>
/// <param name="lumaBlockColumnCount">The number of 8x8 blocks in an aligned luma row.</param>
private void FilterPlane(
Av1Plane plane,
int subsamplingX,
int subsamplingY,
Span<int> directions,
Span<int> variances,
int lumaBlockColumnCount)
{
int planeWidth = this.frameHeader.ModeInfoColumnCount << (Av1Constants.ModeInfoSizeLog2 - subsamplingX);
int planeHeight = this.frameHeader.ModeInfoRowCount << (Av1Constants.ModeInfoSizeLog2 - subsamplingY);
int sourceStride = planeWidth + (SourceBorder * 2);
int sourceLength = (planeHeight + (SourceBorder * 2)) * sourceStride;
using IMemoryOwner<ushort> sourceOwner = this.frameBuffer.MemoryAllocator.Allocate<ushort>(sourceLength);
Span<ushort> source = sourceOwner.Memory.Span[..sourceLength];
// CDEF output must never become input to a later block. The sentinel border also makes
// frame-edge taps follow AV1 without exposing the frame buffer's prediction padding. Every
// sample in the requested working allocation is initialized before any filter can read it.
source.Fill(Av1CdefFilter.VeryLarge);
Span<byte> lowBitDepthDestination = default;
Span<ushort> highBitDepthDestination = default;
int destinationStride;
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);
}
int sourceOffset = (SourceBorder * sourceStride) + SourceBorder;
int destinationOffset = destinationStride;
if (this.frameBuffer.BytesPerSample == 2)
{
Av1CdefFilter.CopyPlane(
highBitDepthDestination,
destinationOffset,
destinationStride,
source,
sourceOffset,
sourceStride,
planeWidth,
planeHeight);
}
else
{
Av1CdefFilter.CopyPlane(
lowBitDepthDestination,
destinationOffset,
destinationStride,
source,
sourceOffset,
sourceStride,
planeWidth,
planeHeight);
} }
ObuConstraintDirectionalEnhancementFilterParameters parameters = this.frameHeader.CdefParameters; int scratchLength = SourceBufferLength + lineBufferLength + columnBufferLength;
int coefficientShift = Math.Max(this.frameBuffer.BitDepth.GetBitCount() - 8, 0); MemoryAllocator allocator = this.frameBuffer.MemoryAllocator;
int blockWidth = 8 >> subsamplingX; using IMemoryOwner<ushort> scratchOwner = allocator.Allocate<ushort>(scratchLength);
int blockHeight = 8 >> subsamplingY; 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 unitColumnCount = (this.frameHeader.ModeInfoColumnCount + CdefUnitModeInfoSize - 1) / CdefUnitModeInfoSize;
int unitRowCount = (this.frameHeader.ModeInfoRowCount + 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++) 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++) for (int unitColumn = 0; unitColumn < unitColumnCount; unitColumn++)
{ {
int unitModeInfoRow = unitRow * CdefUnitModeInfoSize;
int unitModeInfoColumn = unitColumn * CdefUnitModeInfoSize; int unitModeInfoColumn = unitColumn * CdefUnitModeInfoSize;
int strengthIndex = this.GetStrengthIndex(unitModeInfoColumn, unitModeInfoRow); int strengthIndex = this.GetStrengthIndex(unitModeInfoColumn, unitModeInfoRow);
if (strengthIndex < 0) if (strengthIndex < 0)
{ {
cdefLeft.Clear();
continue; continue;
} }
int yStrength = parameters.YStrength[strengthIndex]; int yStrength = parameters.YStrength[strengthIndex];
int uvStrength = parameters.UvStrength[strengthIndex]; int uvStrength = parameters.UvStrength[strengthIndex];
bool unitNeedsDirections = yStrength != 0 || bool unitNeedsDirections = yStrength != 0 || (planeCount > 1 && uvStrength != 0);
(this.sequenceHeader.ColorConfig.PlaneCount > 1 && uvStrength != 0); if (!unitNeedsDirections)
if ((plane == Av1Plane.Y && !unitNeedsDirections) || (plane != Av1Plane.Y && uvStrength == 0))
{ {
cdefLeft.Clear();
continue; continue;
} }
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);
int unitModeInfoRowEnd = Math.Min(unitModeInfoRow + CdefUnitModeInfoSize, this.frameHeader.ModeInfoRowCount); int unitModeInfoRowEnd = Math.Min(unitModeInfoRow + CdefUnitModeInfoSize, this.frameHeader.ModeInfoRowCount);
int unitModeInfoColumnEnd = Math.Min(unitModeInfoColumn + CdefUnitModeInfoSize, this.frameHeader.ModeInfoColumnCount); int unitModeInfoColumnEnd = Math.Min(unitModeInfoColumn + CdefUnitModeInfoSize, this.frameHeader.ModeInfoColumnCount);
CdefBlockList blocks = default; CdefBlockList blocks = default;
@ -253,115 +233,396 @@ internal class Av1CdefDecoder
} }
} }
if (plane == Av1Plane.Y) if (blockCount == 0)
{ {
int blockIndex = 0; cdefLeft.Clear();
continue;
}
// the reference decoder analyzes two listed 8x8 blocks together. The per-unit fixed list preserves that traversal // Luma is always prepared first when either plane type needs CDEF because it owns
// without allocating a managed block list or repeating four skip-map lookups during filtering. // the direction search. Chroma then reuses those per-unit results without a frame map.
for (; blockIndex < blockCount - 1; blockIndex += 2) for (int planeIndex = 0; planeIndex < planeCount; planeIndex++)
{
if (planeIndex != (int)Av1Plane.Y && uvStrength == 0)
{ {
CdefBlock firstBlock = blocks[blockIndex]; cdefLeft[planeIndex] = false;
CdefBlock secondBlock = blocks[blockIndex + 1]; continue;
int firstDirectionIndex = firstBlock.GetDirectionIndex(lumaBlockColumnCount);
int secondDirectionIndex = secondBlock.GetDirectionIndex(lumaBlockColumnCount);
Av1CdefFilter.FindDirections(
source,
firstBlock.GetSourceOffset(sourceStride, SourceBorder),
secondBlock.GetSourceOffset(sourceStride, SourceBorder),
sourceStride,
coefficientShift,
out directions[firstDirectionIndex],
out variances[firstDirectionIndex],
out directions[secondDirectionIndex],
out variances[secondDirectionIndex]);
} }
if (blockIndex < blockCount) int planeWidth = planeWidths[planeIndex];
{ int currentLineSlotOffset = lineBufferOffsets[planeIndex] +
CdefBlock block = blocks[blockIndex]; (((unitRow - 1) & 1) * SourceBorder * planeWidth);
int directionIndex = block.GetDirectionIndex(lumaBlockColumnCount); ReadOnlySpan<ushort> topLineBuffer = unitRow == 0
? default
directions[directionIndex] = Av1CdefFilter.FindDirection( : lineBuffer.Slice(currentLineSlotOffset, SourceBorder * planeWidth);
source, Span<ushort> planeColumnBuffer = columnBuffer.Slice(
block.GetSourceOffset(sourceStride, SourceBorder), columnBufferOffsets[planeIndex],
sourceStride, columnBufferLengths[planeIndex]);
coefficientShift,
out variances[directionIndex]); 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;
} }
}
}
}
for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) /// <summary>
{ /// Filters one color plane in a CDEF unit from a bounded immutable source snapshot.
CdefBlock block = blocks[blockIndex]; /// </summary>
int directionIndex = block.GetDirectionIndex(lumaBlockColumnCount); /// <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);
if (codedStrength == 0) this.GetPlaneDestination(
{ plane,
continue; subsamplingX,
} subsamplingY,
out Span<byte> lowBitDepthDestination,
out Span<ushort> highBitDepthDestination,
out int destinationStride);
int filteredPrimaryStrength = plane == Av1Plane.Y if (hasTop)
? Av1CdefFilter.AdjustStrength(primaryStrength, variances[directionIndex]) {
: primaryStrength; Av1CdefFilter.CopyPlane(
topLineBuffer,
copyColumn,
planeWidth,
source,
sourceColumn,
SourceStride,
copyWidth,
SourceBorder);
}
if (filteredPrimaryStrength == 0 && secondaryStrength == 0) this.CopyFrameRectangle(
{ lowBitDepthDestination,
continue; 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);
}
// Secondary-only filtering uses direction zero; otherwise chroma remaps the int preservedHeight = SourceBorder + unitHeight + (hasBottom ? SourceBorder : 0);
// luma direction into its asymmetrically subsampled sample grid when required. if (leftPrepared)
int direction = primaryStrength != 0 {
? Av1CdefFilter.ConvertDirection(directions[directionIndex], subsamplingX, subsamplingY) Av1CdefFilter.CopyPlane(
: 0; columnBuffer,
int planeColumn = (block.ModeInfoColumn << Av1Constants.ModeInfoSizeLog2) >> subsamplingX; 0,
int planeRow = (block.ModeInfoRow << Av1Constants.ModeInfoSizeLog2) >> subsamplingY; SourceBorder,
int blockSourceOffset = ((planeRow + SourceBorder) * sourceStride) + planeColumn + SourceBorder; source,
int blockDestinationOffset = destinationStride + (planeRow * destinationStride) + planeColumn; 0,
SourceStride,
if (this.frameBuffer.BytesPerSample == 2) SourceBorder,
{ preservedHeight);
Av1CdefFilter.FilterBlock( }
source,
blockSourceOffset, // Save the final unfiltered columns before this unit writes its destination. The next unit
sourceStride, // restores them over the frame samples that this unit has already replaced.
highBitDepthDestination, Av1CdefFilter.CopyPlane(
blockDestinationOffset, source,
destinationStride, unitWidth,
filteredPrimaryStrength, SourceStride,
secondaryStrength, columnBuffer,
direction, 0,
damping, SourceBorder,
damping, SourceBorder,
coefficientShift, preservedHeight);
blockWidth,
blockHeight); ObuConstraintDirectionalEnhancementFilterParameters parameters = this.frameHeader.CdefParameters;
} int coefficientShift = Math.Max(this.frameBuffer.BitDepth.GetBitCount() - 8, 0);
else int blockWidth = 8 >> subsamplingX;
{ int blockHeight = 8 >> subsamplingY;
Av1CdefFilter.FilterBlock( int codedStrength = plane == Av1Plane.Y ? yStrength : uvStrength;
source, int primaryStrength = (codedStrength / 4) << coefficientShift;
blockSourceOffset, int secondaryStrength = codedStrength % 4;
sourceStride,
lowBitDepthDestination, // The two-bit secondary field leaves value three unused and represents strength four instead.
blockDestinationOffset, secondaryStrength += secondaryStrength == 3 ? 1 : 0;
destinationStride, secondaryStrength <<= coefficientShift;
filteredPrimaryStrength, int damping = parameters.Damping + coefficientShift - (plane == Av1Plane.Y ? 0 : 1);
secondaryStrength,
direction, if (plane == Av1Plane.Y)
damping, {
damping, int blockIndex = 0;
coefficientShift,
blockWidth, // The reference decoder analyzes two listed 8x8 blocks together. The per-unit fixed list preserves that traversal
blockHeight); // 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> /// <summary>
/// Gets the strength-table selection assigned to a 64x64 CDEF unit. /// Gets the strength-table selection assigned to a 64x64 CDEF unit.
/// </summary> /// </summary>
@ -445,23 +706,25 @@ internal class Av1CdefDecoder
public int ModeInfoRow { get; } public int ModeInfoRow { get; }
/// <summary> /// <summary>
/// Gets the frame-wide direction-map index for this block. /// Gets the block offset in a bordered CDEF source unit.
/// </summary>
/// <param name="lumaBlockColumnCount">The number of 8x8 blocks in an aligned luma row.</param>
/// <returns>The direction-map index.</returns>
public int GetDirectionIndex(int lumaBlockColumnCount)
=> ((this.ModeInfoRow >> 1) * lumaBlockColumnCount) + (this.ModeInfoColumn >> 1);
/// <summary>
/// Gets the offset of this luma block in the bordered CDEF source plane.
/// </summary> /// </summary>
/// <param name="sourceStride">The number of samples between adjacent source rows.</param> /// <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="sourceBorder">The number of unavailable samples surrounding the source.</param>
/// <returns>The source-plane offset.</returns> /// <param name="unitModeInfoColumn">The unit's frame-relative column in 4x4 luma units.</param>
public int GetSourceOffset(int sourceStride, int sourceBorder) /// <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 << Av1Constants.ModeInfoSizeLog2; int planeColumn = ((this.ModeInfoColumn - unitModeInfoColumn) << Av1Constants.ModeInfoSizeLog2) >> subsamplingX;
int planeRow = this.ModeInfoRow << Av1Constants.ModeInfoSizeLog2; int planeRow = ((this.ModeInfoRow - unitModeInfoRow) << Av1Constants.ModeInfoSizeLog2) >> subsamplingY;
return ((planeRow + sourceBorder) * sourceStride) + planeColumn + sourceBorder; return ((planeRow + sourceBorder) * sourceStride) + planeColumn + sourceBorder;
} }
} }

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

@ -94,14 +94,14 @@ internal sealed class Av1FilmGrainDecoder
int alignedWidth = Av1Math.AlignPowerOf2(visibleWidth, 1); int alignedWidth = Av1Math.AlignPowerOf2(visibleWidth, 1);
int alignedHeight = Av1Math.AlignPowerOf2(visibleHeight, 1); int alignedHeight = Av1Math.AlignPowerOf2(visibleHeight, 1);
Buffer2D<byte> lumaBuffer = this.frameBuffer.BufferY!; Buffer2D<byte> lumaBuffer = this.frameBuffer.GetPlaneBuffer(Av1Plane.Y);
// Frame planes are allocated as bytes even for high-bit-depth pictures. Convert their byte strides to // Frame planes are allocated as bytes even for high-bit-depth pictures. Convert their byte strides to
// native sample strides once so every later offset is expressed consistently in samples. // native sample strides once so every later offset is expressed consistently in samples.
int lumaStride = lumaBuffer.Width / this.frameBuffer.BytesPerSample; int lumaStride = lumaBuffer.Width / this.frameBuffer.BytesPerSample;
int chromaStride = isMonochrome int chromaStride = isMonochrome
? 0 ? 0
: this.frameBuffer.BufferCb!.Width / this.frameBuffer.BytesPerSample; : this.frameBuffer.GetPlaneBuffer(Av1Plane.U).Width / this.frameBuffer.BytesPerSample;
// Closing ApplyGrain over byte or ushort keeps synthesis in the frame buffer's native representation. // Closing ApplyGrain over byte or ushort keeps synthesis in the frame buffer's native representation.
// This avoids an intermediate converted image while allowing the JIT to remove the sample-type branches. // This avoids an intermediate converted image while allowing the JIT to remove the sample-type branches.
@ -115,14 +115,14 @@ internal sealed class Av1FilmGrainDecoder
Span<ushort> cb = isMonochrome Span<ushort> cb = isMonochrome
? Span<ushort>.Empty ? Span<ushort>.Empty
: GetPlaneSamples<ushort>( : GetPlaneSamples<ushort>(
this.frameBuffer.BufferCb!, this.frameBuffer.GetPlaneBuffer(Av1Plane.U),
this.frameBuffer.OriginX >> subsamplingX, this.frameBuffer.OriginX >> subsamplingX,
this.frameBuffer.OriginY >> subsamplingY); this.frameBuffer.OriginY >> subsamplingY);
Span<ushort> cr = isMonochrome Span<ushort> cr = isMonochrome
? Span<ushort>.Empty ? Span<ushort>.Empty
: GetPlaneSamples<ushort>( : GetPlaneSamples<ushort>(
this.frameBuffer.BufferCr!, this.frameBuffer.GetPlaneBuffer(Av1Plane.V),
this.frameBuffer.OriginX >> subsamplingX, this.frameBuffer.OriginX >> subsamplingX,
this.frameBuffer.OriginY >> subsamplingY); this.frameBuffer.OriginY >> subsamplingY);
@ -160,14 +160,14 @@ internal sealed class Av1FilmGrainDecoder
Span<byte> cb = isMonochrome Span<byte> cb = isMonochrome
? Span<byte>.Empty ? Span<byte>.Empty
: GetPlaneSamples<byte>( : GetPlaneSamples<byte>(
this.frameBuffer.BufferCb!, this.frameBuffer.GetPlaneBuffer(Av1Plane.U),
this.frameBuffer.OriginX >> subsamplingX, this.frameBuffer.OriginX >> subsamplingX,
this.frameBuffer.OriginY >> subsamplingY); this.frameBuffer.OriginY >> subsamplingY);
Span<byte> cr = isMonochrome Span<byte> cr = isMonochrome
? Span<byte>.Empty ? Span<byte>.Empty
: GetPlaneSamples<byte>( : GetPlaneSamples<byte>(
this.frameBuffer.BufferCr!, this.frameBuffer.GetPlaneBuffer(Av1Plane.V),
this.frameBuffer.OriginX >> subsamplingX, this.frameBuffer.OriginX >> subsamplingX,
this.frameBuffer.OriginY >> subsamplingY); this.frameBuffer.OriginY >> subsamplingY);

143
src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterContext.cs

@ -4,52 +4,83 @@
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter;
/// <summary> /// <summary>
/// Stores the transform-size map consumed by the AV1 deblocking loop filter. /// Stores the transform-size map consumed by the AV1 deblocking loop filter.
/// </summary> /// </summary>
internal class Av1LoopFilterContext internal sealed class Av1LoopFilterContext : IDisposable
{ {
/// <summary> /// <summary>
/// Stores luma and shared-chroma transform sizes at plane-relative 4x4 granularity. /// Stores luma transform sizes at plane-relative 4x4 granularity.
/// </summary> /// </summary>
private readonly Av1TransformSize[][] transformSizes = new Av1TransformSize[2][]; private readonly MemoryGroup<Av1TransformSize> transformSizesY;
/// <summary> /// <summary>
/// Stores the row stride of each transform-size map. /// The active luma transform-map dimensions in plane-relative 4x4 units.
/// </summary> /// </summary>
private readonly int[] transformSizeStrides = new int[2]; private readonly Size transformSizesYSize;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1LoopFilterContext"/> class. /// Stores shared-chroma transform sizes at plane-relative 4x4 granularity.
/// </summary> /// </summary>
/// <param name="sequenceHeader">The sequence header defining aligned frame and chroma dimensions.</param> private readonly MemoryGroup<Av1TransformSize>? transformSizesUv;
public Av1LoopFilterContext(ObuSequenceHeader sequenceHeader)
{
int alignedModeInfoWidth = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, sequenceHeader.SuperblockSizeLog2) >>
Av1Constants.ModeInfoSizeLog2;
int alignedModeInfoHeight = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameHeight, sequenceHeader.SuperblockSizeLog2) >> /// <summary>
Av1Constants.ModeInfoSizeLog2; /// The active shared-chroma transform-map dimensions in plane-relative 4x4 units.
/// </summary>
private readonly Size transformSizesUvSize;
this.transformSizeStrides[(int)Av1PlaneType.Y] = alignedModeInfoWidth; /// <summary>
this.transformSizes[(int)Av1PlaneType.Y] = new Av1TransformSize[alignedModeInfoWidth * alignedModeInfoHeight]; /// Initializes a new instance of the <see cref="Av1LoopFilterContext"/> class.
/// </summary>
/// <param name="memoryAllocator">The allocator that owns the frame-sized transform maps.</param>
/// <param name="sequenceHeader">The sequence header defining superblock and chroma geometry.</param>
/// <param name="frameHeader">The frame header defining active coded dimensions.</param>
public Av1LoopFilterContext(
MemoryAllocator memoryAllocator,
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader)
{
int modeInfoWidth = frameHeader.ModeInfoColumnCount;
int modeInfoHeight = frameHeader.ModeInfoRowCount;
MemoryGroup<Av1TransformSize>? transformSizesY = null;
MemoryGroup<Av1TransformSize>? transformSizesUv = null;
this.transformSizesUvSize = default;
if (!sequenceHeader.ColorConfig.IsMonochrome) try
{ {
int subX = sequenceHeader.ColorConfig.SubSamplingX ? 1 : 0; long lumaLength = (long)modeInfoWidth * modeInfoHeight;
int subY = sequenceHeader.ColorConfig.SubSamplingY ? 1 : 0; transformSizesY = memoryAllocator.AllocateGroup<Av1TransformSize>(
int chromaWidth = Av1Math.DivideLog2Ceiling(alignedModeInfoWidth, subX); lumaLength,
int chromaHeight = Av1Math.DivideLog2Ceiling(alignedModeInfoHeight, subY); 1,
AllocationOptions.Clean);
this.transformSizeStrides[(int)Av1PlaneType.Uv] = chromaWidth; if (!sequenceHeader.ColorConfig.IsMonochrome)
this.transformSizes[(int)Av1PlaneType.Uv] = new Av1TransformSize[chromaWidth * chromaHeight]; {
int subX = sequenceHeader.ColorConfig.SubSamplingX ? 1 : 0;
int subY = sequenceHeader.ColorConfig.SubSamplingY ? 1 : 0;
int chromaWidth = Av1Math.DivideLog2Ceiling(modeInfoWidth, subX);
int chromaHeight = Av1Math.DivideLog2Ceiling(modeInfoHeight, subY);
long chromaLength = (long)chromaWidth * chromaHeight;
transformSizesUv = memoryAllocator.AllocateGroup<Av1TransformSize>(
chromaLength,
1,
AllocationOptions.Clean);
this.transformSizesUvSize = new Size(chromaWidth, chromaHeight);
}
this.transformSizesY = transformSizesY;
this.transformSizesYSize = new Size(modeInfoWidth, modeInfoHeight);
this.transformSizesUv = transformSizesUv;
} }
else catch
{ {
this.transformSizes[(int)Av1PlaneType.Uv] = []; transformSizesUv?.Dispose();
transformSizesY?.Dispose();
throw;
} }
} }
@ -62,16 +93,38 @@ internal class Av1LoopFilterContext
public void SetTransformSize(Av1Plane plane, Point position, Av1TransformSize transformSize) public void SetTransformSize(Av1Plane plane, Point position, Av1TransformSize transformSize)
{ {
int planeType = Math.Min((int)plane, (int)Av1PlaneType.Uv); int planeType = Math.Min((int)plane, (int)Av1PlaneType.Uv);
Span<Av1TransformSize> transformSizeMap = this.transformSizes[planeType]; MemoryGroup<Av1TransformSize> transformSizeMap;
int stride = this.transformSizeStrides[planeType]; Size transformSizeMapSize;
int width = transformSize.Get4x4WideCount(); if (planeType == (int)Av1PlaneType.Y)
int height = transformSize.Get4x4HighCount(); {
transformSizeMap = this.transformSizesY;
transformSizeMapSize = this.transformSizesYSize;
}
else
{
transformSizeMap = this.transformSizesUv
?? throw new InvalidOperationException("A monochrome AV1 frame has no chroma transform-size map.");
// Loop filtering addresses every covered 4x4 position, not only the transform origin. Replication keeps transformSizeMapSize = this.transformSizesUvSize;
// edge lookup independent of the transform traversal order used while reconstructing the coded block. }
int width = Math.Min(transformSize.Get4x4WideCount(), transformSizeMapSize.Width - position.X);
int height = Math.Min(transformSize.Get4x4HighCount(), transformSizeMapSize.Height - position.Y);
// libaom clips transform coverage to the active plane mi dimensions at frame edges. Each logical row may cross
// allocator segments, so fill only the current segment before continuing at the same logical map offset.
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
transformSizeMap.Slice(((position.Y + y) * stride) + position.X, width).Fill(transformSize); long offset = ((long)(position.Y + y) * transformSizeMapSize.Width) + position.X;
int remaining = width;
while (remaining > 0)
{
Span<Av1TransformSize> destination = transformSizeMap.GetRemainingSliceOfBuffer(offset);
int count = Math.Min(remaining, destination.Length);
destination[..count].Fill(transformSize);
offset += count;
remaining -= count;
}
} }
} }
@ -84,7 +137,31 @@ internal class Av1LoopFilterContext
public Av1TransformSize GetTransformSize(Av1Plane plane, Point position) public Av1TransformSize GetTransformSize(Av1Plane plane, Point position)
{ {
int planeType = Math.Min((int)plane, (int)Av1PlaneType.Uv); int planeType = Math.Min((int)plane, (int)Av1PlaneType.Uv);
int stride = this.transformSizeStrides[planeType]; MemoryGroup<Av1TransformSize> transformSizeMap;
return this.transformSizes[planeType][(position.Y * stride) + position.X]; int width;
if (planeType == (int)Av1PlaneType.Y)
{
transformSizeMap = this.transformSizesY;
width = this.transformSizesYSize.Width;
}
else
{
transformSizeMap = this.transformSizesUv
?? throw new InvalidOperationException("A monochrome AV1 frame has no chroma transform-size map.");
width = this.transformSizesUvSize.Width;
}
long offset = ((long)position.Y * width) + position.X;
return transformSizeMap.GetRemainingSliceOfBuffer(offset)[0];
}
/// <summary>
/// Returns the allocator-owned transform-size maps.
/// </summary>
public void Dispose()
{
this.transformSizesUv?.Dispose();
this.transformSizesY.Dispose();
} }
} }

4
src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterDecoder.cs

@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter;
/// <summary> /// <summary>
/// Applies the AV1 in-loop deblocking stage to a reconstructed still-image frame. /// Applies the AV1 in-loop deblocking stage to a reconstructed still-image frame.
/// </summary> /// </summary>
internal class Av1LoopFilterDecoder internal sealed class Av1LoopFilterDecoder
{ {
/// <summary> /// <summary>
/// The sequence-level superblock and color configuration. /// The sequence-level superblock and color configuration.
@ -365,7 +365,7 @@ internal class Av1LoopFilterDecoder
level = Av1Math.Clip3( level = Av1Math.Clip3(
0, 0,
Av1Constants.MaxLoopFilter, Av1Constants.MaxLoopFilter,
level + segmentation.FeatureData[modeInfo.SegmentId, (int)feature]); level + segmentation.GetFeatureData(modeInfo.SegmentId, (int)feature));
} }
if (parameters.ReferenceDeltaModeEnabled) if (parameters.ReferenceDeltaModeEnabled)

17
src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationBoundary.cs

@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration;
/// <summary> /// <summary>
/// Preserves the deblocked and frame-edge rows required by AV1 striped loop restoration. /// Preserves the deblocked and frame-edge rows required by AV1 striped loop restoration.
/// </summary> /// </summary>
internal class Av1LoopRestorationBoundary : IDisposable internal sealed class Av1LoopRestorationBoundary : IDisposable
{ {
/// <summary> /// <summary>
/// The height of a complete restoration processing stripe in luma samples. /// The height of a complete restoration processing stripe in luma samples.
@ -46,22 +46,22 @@ internal class Av1LoopRestorationBoundary : IDisposable
/// <summary> /// <summary>
/// The two preserved rows above every processing stripe, stored by plane. /// The two preserved rows above every processing stripe, stored by plane.
/// </summary> /// </summary>
private readonly IMemoryOwner<ushort>?[] rowsAbove = new IMemoryOwner<ushort>?[Av1Constants.MaxPlanes]; private InlineArray4<IMemoryOwner<ushort>?> rowsAbove;
/// <summary> /// <summary>
/// The two preserved rows below every processing stripe, stored by plane. /// The two preserved rows below every processing stripe, stored by plane.
/// </summary> /// </summary>
private readonly IMemoryOwner<ushort>?[] rowsBelow = new IMemoryOwner<ushort>?[Av1Constants.MaxPlanes]; private InlineArray4<IMemoryOwner<ushort>?> rowsBelow;
/// <summary> /// <summary>
/// The upscaled sample width stored for each plane boundary row. /// The upscaled sample width stored for each plane boundary row.
/// </summary> /// </summary>
private readonly int[] planeWidths = new int[Av1Constants.MaxPlanes]; private InlineArray4<int> planeWidths;
/// <summary> /// <summary>
/// The number of processing stripes represented for each plane. /// The number of processing stripes represented for each plane.
/// </summary> /// </summary>
private readonly int[] stripeCounts = new int[Av1Constants.MaxPlanes]; private InlineArray4<int> stripeCounts;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1LoopRestorationBoundary"/> class. /// Initializes a new instance of the <see cref="Av1LoopRestorationBoundary"/> class.
@ -430,13 +430,16 @@ internal class Av1LoopRestorationBoundary : IDisposable
/// <param name="contextRow">The first or second preserved context row.</param> /// <param name="contextRow">The first or second preserved context row.</param>
/// <returns>The selected boundary-row span.</returns> /// <returns>The selected boundary-row span.</returns>
private Span<ushort> GetBoundaryRow( private Span<ushort> GetBoundaryRow(
IMemoryOwner<ushort>?[] storage, ReadOnlySpan<IMemoryOwner<ushort>?> storage,
int plane, int plane,
int stripe, int stripe,
int contextRow) int contextRow)
{ {
int width = this.planeWidths[plane]; int width = this.planeWidths[plane];
int offset = ((stripe * ContextRowCount) + contextRow) * width; int offset = ((stripe * ContextRowCount) + contextRow) * width;
return storage[plane]!.Memory.Span.Slice(offset, width); IMemoryOwner<ushort> owner = storage[plane]
?? throw new InvalidOperationException("The selected AV1 plane has no loop-restoration boundary storage.");
return owner.Memory.Span.Slice(offset, width);
} }
} }

332
src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationDecoder.cs

@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration;
/// <summary> /// <summary>
/// Applies decoded AV1 loop-restoration units to a reconstructed still-image frame. /// Applies decoded AV1 loop-restoration units to a reconstructed still-image frame.
/// </summary> /// </summary>
internal class Av1LoopRestorationDecoder internal sealed class Av1LoopRestorationDecoder
{ {
/// <summary> /// <summary>
/// The number of source rows and columns required around each filtered processing stripe. /// The number of source rows and columns required around each filtered processing stripe.
@ -107,14 +107,42 @@ internal class Av1LoopRestorationDecoder
int planeHeight = Av1Math.DivideLog2Ceiling(frameSize.FrameHeight, subsamplingY); int planeHeight = Av1Math.DivideLog2Ceiling(frameSize.FrameHeight, subsamplingY);
int planeLength = planeWidth * planeHeight; int planeLength = planeWidth * planeHeight;
MemoryAllocator allocator = this.frameBuffer.MemoryAllocator; MemoryAllocator allocator = this.frameBuffer.MemoryAllocator;
using IMemoryOwner<ushort> sourceOwner = allocator.Allocate<ushort>(planeLength); ReadOnlySpan<byte> lowBitDepthSource = default;
using IMemoryOwner<ushort> destinationOwner = allocator.Allocate<ushort>(planeLength); ReadOnlySpan<ushort> highBitDepthSource = default;
Span<ushort> source = sourceOwner.Memory.Span[..planeLength]; int sourceStride;
Span<ushort> destination = destinationOwner.Memory.Span[..planeLength]; if (this.frameBuffer.BytesPerSample == 2)
{
Span<short> signedPlane = this.frameBuffer.DeriveBlockPointer16(
plane,
Point.Empty,
subsamplingX,
subsamplingY,
out sourceStride);
// Restoration units overlap in their filter context but not in their output. A separate highBitDepthSource = MemoryMarshal.Cast<short, ushort>(signedPlane);
// destination prevents later units from observing already restored neighboring samples. }
this.CopyPlaneToWorkingBuffer(plane, subsamplingX, subsamplingY, planeWidth, planeHeight, source); else
{
lowBitDepthSource = this.frameBuffer.DeriveBlockPointer(
plane,
Point.Empty,
subsamplingX,
subsamplingY,
out sourceStride);
}
int destinationStorageLength = planeLength * this.frameBuffer.BytesPerSample;
using IMemoryOwner<byte> destinationOwner = allocator.Allocate<byte>(destinationStorageLength);
Span<byte> destinationStorage = destinationOwner.Memory.Span[..destinationStorageLength];
Span<byte> lowBitDepthDestination = this.frameBuffer.BytesPerSample == 1
? destinationStorage
: default;
Span<ushort> highBitDepthDestination = this.frameBuffer.BytesPerSample == 2
? MemoryMarshal.Cast<byte, ushort>(destinationStorage)
: default;
// Restoration units overlap in their filter context but not in their output. Reading the reconstructed
// plane directly while writing a native-width destination matches libaom's frame/rst_frame ownership.
// AV1 lets the last unit absorb a remainder smaller than 150 percent of the nominal size. // AV1 lets the last unit absorb a remainder smaller than 150 percent of the nominal size.
// Size scratch storage for that largest legal unit rather than the nominal grid step. // Size scratch storage for that largest legal unit rather than the nominal grid step.
@ -124,10 +152,15 @@ internal class Av1LoopRestorationDecoder
int borderedStride = maximumUnitWidth + (FilterBorder * 2) + WienerPadding; int borderedStride = maximumUnitWidth + (FilterBorder * 2) + WienerPadding;
int borderedLength = borderedStride * (maximumStripeHeight + (FilterBorder * 2) + WienerPadding); int borderedLength = borderedStride * (maximumStripeHeight + (FilterBorder * 2) + WienerPadding);
int wienerScratchLength = Av1WienerFilter.GetScratchLength(maximumUnitWidth, maximumStripeHeight); int wienerScratchLength = Av1WienerFilter.GetScratchLength(maximumUnitWidth, maximumStripeHeight);
using IMemoryOwner<ushort> borderedSourceOwner = allocator.Allocate<ushort>(borderedLength); int filterOutputLength = this.frameBuffer.BytesPerSample == 1
using IMemoryOwner<ushort> wienerScratchOwner = allocator.Allocate<ushort>(wienerScratchLength); ? maximumUnitWidth * maximumStripeHeight
Span<ushort> borderedSource = borderedSourceOwner.Memory.Span[..borderedLength]; : 0;
Span<ushort> wienerScratch = wienerScratchOwner.Memory.Span[..wienerScratchLength]; int ushortScratchLength = borderedLength + wienerScratchLength + filterOutputLength;
using IMemoryOwner<ushort> ushortScratchOwner = allocator.Allocate<ushort>(ushortScratchLength);
Span<ushort> ushortScratch = ushortScratchOwner.Memory.Span[..ushortScratchLength];
Span<ushort> borderedSource = ushortScratch[..borderedLength];
Span<ushort> wienerScratch = ushortScratch.Slice(borderedLength, wienerScratchLength);
Span<ushort> filterOutput = ushortScratch[(borderedLength + wienerScratchLength)..];
int processingUnitWidth = Av1LoopRestorationBoundary.ProcessingStripeSize >> subsamplingX; int processingUnitWidth = Av1LoopRestorationBoundary.ProcessingStripeSize >> subsamplingX;
int maximumSelfGuidedWidth = Math.Min(processingUnitWidth, maximumUnitWidth); int maximumSelfGuidedWidth = Math.Min(processingUnitWidth, maximumUnitWidth);
int selfGuidedScratchLength = Av1SelfGuidedFilter.GetScratchLength(maximumSelfGuidedWidth, maximumStripeHeight); int selfGuidedScratchLength = Av1SelfGuidedFilter.GetScratchLength(maximumSelfGuidedWidth, maximumStripeHeight);
@ -166,8 +199,11 @@ internal class Av1LoopRestorationDecoder
this.FilterUnit( this.FilterUnit(
planeIndex, planeIndex,
subsamplingX, subsamplingX,
source, lowBitDepthSource,
destination, highBitDepthSource,
sourceStride,
lowBitDepthDestination,
highBitDepthDestination,
planeWidth, planeWidth,
planeHeight, planeHeight,
unitX, unitX,
@ -177,6 +213,7 @@ internal class Av1LoopRestorationDecoder
unit, unit,
borderedSource, borderedSource,
wienerScratch, wienerScratch,
filterOutput,
selfGuidedScratch); selfGuidedScratch);
unitX += unitWidth; unitX += unitWidth;
@ -185,7 +222,14 @@ internal class Av1LoopRestorationDecoder
unitY += unadjustedUnitHeight; unitY += unadjustedUnitHeight;
} }
this.CopyWorkingBufferToPlane(plane, subsamplingX, subsamplingY, planeWidth, planeHeight, destination); this.CopyWorkingBufferToPlane(
plane,
subsamplingX,
subsamplingY,
planeWidth,
planeHeight,
lowBitDepthDestination,
highBitDepthDestination);
} }
/// <summary> /// <summary>
@ -193,8 +237,11 @@ internal class Av1LoopRestorationDecoder
/// </summary> /// </summary>
/// <param name="plane">The zero-based color-plane index.</param> /// <param name="plane">The zero-based color-plane index.</param>
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param> /// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
/// <param name="source">The immutable post-super-resolution plane samples.</param> /// <param name="lowBitDepthSource">The immutable byte post-super-resolution plane, when present.</param>
/// <param name="destination">The restored destination plane samples.</param> /// <param name="highBitDepthSource">The immutable 16-bit post-super-resolution plane, when present.</param>
/// <param name="sourceStride">The number of samples between source rows.</param>
/// <param name="lowBitDepthDestination">The byte restoration destination, when present.</param>
/// <param name="highBitDepthDestination">The native 16-bit restoration destination, when present.</param>
/// <param name="planeWidth">The visible plane width.</param> /// <param name="planeWidth">The visible plane width.</param>
/// <param name="planeHeight">The visible plane height.</param> /// <param name="planeHeight">The visible plane height.</param>
/// <param name="horizontalStart">The unit's first plane column.</param> /// <param name="horizontalStart">The unit's first plane column.</param>
@ -204,12 +251,16 @@ internal class Av1LoopRestorationDecoder
/// <param name="unit">The decoded unit filter and coefficients.</param> /// <param name="unit">The decoded unit filter and coefficients.</param>
/// <param name="borderedSource">Reusable storage for one bordered processing stripe.</param> /// <param name="borderedSource">Reusable storage for one bordered processing stripe.</param>
/// <param name="wienerScratch">Reusable Wiener intermediate storage.</param> /// <param name="wienerScratch">Reusable Wiener intermediate storage.</param>
/// <param name="filterOutput">Reusable native-precision filter output for an eight-bit frame.</param>
/// <param name="selfGuidedScratch">Reusable self-guided intermediate storage.</param> /// <param name="selfGuidedScratch">Reusable self-guided intermediate storage.</param>
private void FilterUnit( private void FilterUnit(
int plane, int plane,
int subsamplingX, int subsamplingX,
ReadOnlySpan<ushort> source, ReadOnlySpan<byte> lowBitDepthSource,
Span<ushort> destination, ReadOnlySpan<ushort> highBitDepthSource,
int sourceStride,
Span<byte> lowBitDepthDestination,
Span<ushort> highBitDepthDestination,
int planeWidth, int planeWidth,
int planeHeight, int planeHeight,
int horizontalStart, int horizontalStart,
@ -219,6 +270,7 @@ internal class Av1LoopRestorationDecoder
Av1LoopRestorationUnit unit, Av1LoopRestorationUnit unit,
Span<ushort> borderedSource, Span<ushort> borderedSource,
Span<ushort> wienerScratch, Span<ushort> wienerScratch,
Span<ushort> filterOutput,
Span<int> selfGuidedScratch) Span<int> selfGuidedScratch)
{ {
int unitWidth = horizontalEnd - horizontalStart; int unitWidth = horizontalEnd - horizontalStart;
@ -227,8 +279,11 @@ internal class Av1LoopRestorationDecoder
// Every output sample still belongs to exactly one unit, including units that select // Every output sample still belongs to exactly one unit, including units that select
// RESTORE_NONE, so copy the immutable source rectangle into the destination plane. // RESTORE_NONE, so copy the immutable source rectangle into the destination plane.
CopyRectangle( CopyRectangle(
source, lowBitDepthSource,
destination, highBitDepthSource,
sourceStride,
lowBitDepthDestination,
highBitDepthDestination,
planeWidth, planeWidth,
horizontalStart, horizontalStart,
unitWidth, unitWidth,
@ -253,35 +308,53 @@ internal class Av1LoopRestorationDecoder
// 64 luma samples high, with the current unit limiting only the final iteration. // 64 luma samples high, with the current unit limiting only the final iteration.
if (unit.FilterType == Av1RestorationFilterType.Wiener) if (unit.FilterType == Av1RestorationFilterType.Wiener)
{ {
int sourceStride = unitWidth + (FilterBorder * 2) + WienerPadding; int borderedStride = unitWidth + (FilterBorder * 2) + WienerPadding;
int sourceLength = sourceStride * (stripeHeight + (FilterBorder * 2) + WienerPadding); int sourceLength = borderedStride * (stripeHeight + (FilterBorder * 2) + WienerPadding);
Span<ushort> filterSource = borderedSource[..sourceLength]; Span<ushort> filterSource = borderedSource[..sourceLength];
this.PopulateBorderedSource( this.PopulateBorderedSource(
plane, plane,
frameStripe, frameStripe,
source, lowBitDepthSource,
highBitDepthSource,
sourceStride,
planeWidth, planeWidth,
planeHeight, planeHeight,
horizontalStart, horizontalStart,
unitWidth, unitWidth,
stripeStart, stripeStart,
stripeHeight, stripeHeight,
sourceStride, borderedStride,
filterSource); filterSource);
int destinationOffset = (stripeStart * planeWidth) + horizontalStart; int destinationOffset = (stripeStart * planeWidth) + horizontalStart;
Span<ushort> filterDestination = highBitDepthDestination.IsEmpty
? filterOutput[..(unitWidth * stripeHeight)]
: highBitDepthDestination[destinationOffset..];
int filterDestinationStride = highBitDepthDestination.IsEmpty ? unitWidth : planeWidth;
int scratchLength = Av1WienerFilter.GetScratchLength(unitWidth, stripeHeight); int scratchLength = Av1WienerFilter.GetScratchLength(unitWidth, stripeHeight);
Av1WienerFilter.FilterStripe( Av1WienerFilter.FilterStripe(
filterSource, filterSource,
sourceStride, borderedStride,
destination[destinationOffset..], filterDestination,
planeWidth, filterDestinationStride,
unitWidth, unitWidth,
stripeHeight, stripeHeight,
this.frameBuffer.BitDepth.GetBitCount(), this.frameBuffer.BitDepth.GetBitCount(),
unit.WienerHorizontal, unit.WienerHorizontal,
unit.WienerVertical, unit.WienerVertical,
wienerScratch[..scratchLength]); wienerScratch[..scratchLength]);
if (highBitDepthDestination.IsEmpty)
{
CopyFilterOutput(
filterDestination,
filterDestinationStride,
lowBitDepthDestination,
destinationOffset,
planeWidth,
unitWidth,
stripeHeight);
}
} }
else else
{ {
@ -293,35 +366,53 @@ internal class Av1LoopRestorationDecoder
{ {
int blockWidth = Math.Min(processingUnitWidth, unitWidth - unitColumn); int blockWidth = Math.Min(processingUnitWidth, unitWidth - unitColumn);
int blockStart = horizontalStart + unitColumn; int blockStart = horizontalStart + unitColumn;
int sourceStride = blockWidth + (FilterBorder * 2) + WienerPadding; int borderedStride = blockWidth + (FilterBorder * 2) + WienerPadding;
int sourceLength = sourceStride * (stripeHeight + (FilterBorder * 2) + WienerPadding); int sourceLength = borderedStride * (stripeHeight + (FilterBorder * 2) + WienerPadding);
Span<ushort> filterSource = borderedSource[..sourceLength]; Span<ushort> filterSource = borderedSource[..sourceLength];
this.PopulateBorderedSource( this.PopulateBorderedSource(
plane, plane,
frameStripe, frameStripe,
source, lowBitDepthSource,
highBitDepthSource,
sourceStride,
planeWidth, planeWidth,
planeHeight, planeHeight,
blockStart, blockStart,
blockWidth, blockWidth,
stripeStart, stripeStart,
stripeHeight, stripeHeight,
sourceStride, borderedStride,
filterSource); filterSource);
int destinationOffset = (stripeStart * planeWidth) + blockStart; int destinationOffset = (stripeStart * planeWidth) + blockStart;
Span<ushort> filterDestination = highBitDepthDestination.IsEmpty
? filterOutput[..(blockWidth * stripeHeight)]
: highBitDepthDestination[destinationOffset..];
int filterDestinationStride = highBitDepthDestination.IsEmpty ? blockWidth : planeWidth;
int scratchLength = Av1SelfGuidedFilter.GetScratchLength(blockWidth, stripeHeight); int scratchLength = Av1SelfGuidedFilter.GetScratchLength(blockWidth, stripeHeight);
Av1SelfGuidedFilter.FilterBlock( Av1SelfGuidedFilter.FilterBlock(
filterSource, filterSource,
sourceStride, borderedStride,
destination[destinationOffset..], filterDestination,
planeWidth, filterDestinationStride,
blockWidth, blockWidth,
stripeHeight, stripeHeight,
this.frameBuffer.BitDepth.GetBitCount(), this.frameBuffer.BitDepth.GetBitCount(),
unit.SgrParameterSet, unit.SgrParameterSet,
unit.SgrProjectionCoefficients, unit.SgrProjectionCoefficients,
selfGuidedScratch[..scratchLength]); selfGuidedScratch[..scratchLength]);
if (highBitDepthDestination.IsEmpty)
{
CopyFilterOutput(
filterDestination,
filterDestinationStride,
lowBitDepthDestination,
destinationOffset,
planeWidth,
blockWidth,
stripeHeight);
}
} }
} }
@ -334,7 +425,9 @@ internal class Av1LoopRestorationDecoder
/// </summary> /// </summary>
/// <param name="plane">The zero-based color-plane index.</param> /// <param name="plane">The zero-based color-plane index.</param>
/// <param name="frameStripe">The frame-relative processing-stripe index.</param> /// <param name="frameStripe">The frame-relative processing-stripe index.</param>
/// <param name="source">The immutable post-super-resolution plane samples.</param> /// <param name="lowBitDepthSource">The immutable byte post-super-resolution plane, when present.</param>
/// <param name="highBitDepthSource">The immutable 16-bit post-super-resolution plane, when present.</param>
/// <param name="sourceStride">The number of samples between source rows.</param>
/// <param name="planeWidth">The visible plane width.</param> /// <param name="planeWidth">The visible plane width.</param>
/// <param name="planeHeight">The visible plane height.</param> /// <param name="planeHeight">The visible plane height.</param>
/// <param name="blockStart">The first filtered plane column.</param> /// <param name="blockStart">The first filtered plane column.</param>
@ -346,7 +439,9 @@ internal class Av1LoopRestorationDecoder
private void PopulateBorderedSource( private void PopulateBorderedSource(
int plane, int plane,
int frameStripe, int frameStripe,
ReadOnlySpan<ushort> source, ReadOnlySpan<byte> lowBitDepthSource,
ReadOnlySpan<ushort> highBitDepthSource,
int sourceStride,
int planeWidth, int planeWidth,
int planeHeight, int planeHeight,
int blockStart, int blockStart,
@ -361,30 +456,38 @@ internal class Av1LoopRestorationDecoder
for (int destinationRow = 0; destinationRow < sourceRowCount; destinationRow++) for (int destinationRow = 0; destinationRow < sourceRowCount; destinationRow++)
{ {
int sourceY = stripeStart + destinationRow - FilterBorder; int sourceY = stripeStart + destinationRow - FilterBorder;
ReadOnlySpan<ushort> sourceRow; ReadOnlySpan<byte> lowBitDepthSourceRow = default;
ReadOnlySpan<ushort> highBitDepthSourceRow;
if (sourceY < 0) if (sourceY < 0)
{ {
sourceRow = this.boundary.GetRowAbove(plane, frameStripe, 0); highBitDepthSourceRow = this.boundary.GetRowAbove(plane, frameStripe, 0);
} }
else if (sourceY < stripeStart) else if (sourceY < stripeStart)
{ {
// Two preserved deblocked rows expand to three filter rows as [0, 0, 1]. // Two preserved deblocked rows expand to three filter rows as [0, 0, 1].
int contextRow = Math.Min(Math.Max(destinationRow - 1, 0), 1); int contextRow = Math.Min(Math.Max(destinationRow - 1, 0), 1);
sourceRow = this.boundary.GetRowAbove(plane, frameStripe, contextRow); highBitDepthSourceRow = this.boundary.GetRowAbove(plane, frameStripe, contextRow);
} }
else if (sourceY >= planeHeight) else if (sourceY >= planeHeight)
{ {
sourceRow = this.boundary.GetRowBelow(plane, frameStripe, 0); highBitDepthSourceRow = this.boundary.GetRowBelow(plane, frameStripe, 0);
} }
else if (sourceY >= stripeEnd) else if (sourceY >= stripeEnd)
{ {
// The bottom expansion is [0, 1, 1]; the padded Wiener zero tap also reads row 1. // The bottom expansion is [0, 1, 1]; the padded Wiener zero tap also reads row 1.
int contextRow = Math.Min(sourceY - stripeEnd, 1); int contextRow = Math.Min(sourceY - stripeEnd, 1);
sourceRow = this.boundary.GetRowBelow(plane, frameStripe, contextRow); highBitDepthSourceRow = this.boundary.GetRowBelow(plane, frameStripe, contextRow);
}
else if (!highBitDepthSource.IsEmpty)
{
int sourceOffset = sourceStride + (sourceY * sourceStride);
highBitDepthSourceRow = highBitDepthSource.Slice(sourceOffset, planeWidth);
} }
else else
{ {
sourceRow = source.Slice(sourceY * planeWidth, planeWidth); int sourceOffset = sourceStride + (sourceY * sourceStride);
lowBitDepthSourceRow = lowBitDepthSource.Slice(sourceOffset, planeWidth);
highBitDepthSourceRow = default;
} }
Span<ushort> destinationRowSpan = destination.Slice(destinationRow * destinationStride, destinationStride); Span<ushort> destinationRowSpan = destination.Slice(destinationRow * destinationStride, destinationStride);
@ -395,18 +498,38 @@ internal class Av1LoopRestorationDecoder
// Replication occurs only at the visible frame boundary. // Replication occurs only at the visible frame boundary.
if (leftExtension > 0) if (leftExtension > 0)
{ {
destinationRowSpan[..leftExtension].Fill(sourceRow[0]); ushort firstSample = highBitDepthSourceRow.IsEmpty
? lowBitDepthSourceRow[0]
: highBitDepthSourceRow[0];
destinationRowSpan[..leftExtension].Fill(firstSample);
} }
int copiedStart = Math.Max(sourceX, 0); int copiedStart = Math.Max(sourceX, 0);
int copiedEnd = Math.Min(sourceX + destinationStride, planeWidth); int copiedEnd = Math.Min(sourceX + destinationStride, planeWidth);
int copiedLength = copiedEnd - copiedStart; int copiedLength = copiedEnd - copiedStart;
sourceRow.Slice(copiedStart, copiedLength).CopyTo(destinationRowSpan[leftExtension..]); Span<ushort> copiedDestination = destinationRowSpan.Slice(leftExtension, copiedLength);
if (!highBitDepthSourceRow.IsEmpty)
{
highBitDepthSourceRow.Slice(copiedStart, copiedLength).CopyTo(copiedDestination);
}
else
{
ReadOnlySpan<byte> copiedSource = lowBitDepthSourceRow.Slice(copiedStart, copiedLength);
for (int column = 0; column < copiedLength; column++)
{
copiedDestination[column] = copiedSource[column];
}
}
int populatedLength = leftExtension + copiedLength; int populatedLength = leftExtension + copiedLength;
if (populatedLength < destinationStride) if (populatedLength < destinationStride)
{ {
destinationRowSpan[populatedLength..].Fill(sourceRow[^1]); ushort lastSample = highBitDepthSourceRow.IsEmpty
? lowBitDepthSourceRow[^1]
: highBitDepthSourceRow[^1];
destinationRowSpan[populatedLength..].Fill(lastSample);
} }
} }
} }
@ -414,16 +537,22 @@ internal class Av1LoopRestorationDecoder
/// <summary> /// <summary>
/// Copies an unfiltered restoration-unit rectangle between plane working buffers. /// Copies an unfiltered restoration-unit rectangle between plane working buffers.
/// </summary> /// </summary>
/// <param name="source">The immutable source plane.</param> /// <param name="lowBitDepthSource">The immutable byte source plane, when present.</param>
/// <param name="destination">The destination plane.</param> /// <param name="highBitDepthSource">The immutable 16-bit source plane, when present.</param>
/// <param name="sourceStride">The number of samples between source rows.</param>
/// <param name="lowBitDepthDestination">The byte destination, when present.</param>
/// <param name="highBitDepthDestination">The native 16-bit destination, when present.</param>
/// <param name="planeWidth">The number of samples between plane rows.</param> /// <param name="planeWidth">The number of samples between plane rows.</param>
/// <param name="horizontalStart">The first copied column.</param> /// <param name="horizontalStart">The first copied column.</param>
/// <param name="width">The number of copied columns.</param> /// <param name="width">The number of copied columns.</param>
/// <param name="verticalStart">The first copied row.</param> /// <param name="verticalStart">The first copied row.</param>
/// <param name="verticalEnd">The exclusive copied row limit.</param> /// <param name="verticalEnd">The exclusive copied row limit.</param>
private static void CopyRectangle( private static void CopyRectangle(
ReadOnlySpan<ushort> source, ReadOnlySpan<byte> lowBitDepthSource,
Span<ushort> destination, ReadOnlySpan<ushort> highBitDepthSource,
int sourceStride,
Span<byte> lowBitDepthDestination,
Span<ushort> highBitDepthDestination,
int planeWidth, int planeWidth,
int horizontalStart, int horizontalStart,
int width, int width,
@ -432,89 +561,69 @@ internal class Av1LoopRestorationDecoder
{ {
for (int row = verticalStart; row < verticalEnd; row++) for (int row = verticalStart; row < verticalEnd; row++)
{ {
int offset = (row * planeWidth) + horizontalStart; int sourceOffset = sourceStride + (row * sourceStride) + horizontalStart;
source.Slice(offset, width).CopyTo(destination[offset..]); int destinationOffset = (row * planeWidth) + horizontalStart;
if (!highBitDepthDestination.IsEmpty)
{
highBitDepthSource.Slice(sourceOffset, width).CopyTo(highBitDepthDestination.Slice(destinationOffset, width));
}
else
{
lowBitDepthSource.Slice(sourceOffset, width).CopyTo(lowBitDepthDestination.Slice(destinationOffset, width));
}
} }
} }
/// <summary> /// <summary>
/// Copies one reconstructed plane into an immutable 16-bit working buffer. /// Narrows one bounded restoration-filter output into its eight-bit frame destination.
/// </summary> /// </summary>
/// <param name="plane">The luma or chroma plane.</param> /// <param name="source">The 16-bit filter output.</param>
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param> /// <param name="sourceStride">The number of samples between source rows.</param>
/// <param name="subsamplingY">The vertical chroma subsampling shift.</param> /// <param name="destination">The byte restoration destination.</param>
/// <param name="planeWidth">The visible plane width.</param> /// <param name="destinationOffset">The offset of the first destination sample.</param>
/// <param name="planeHeight">The visible plane height.</param> /// <param name="destinationStride">The number of samples between destination rows.</param>
/// <param name="destination">The row-major working buffer.</param> /// <param name="width">The copied width in samples.</param>
private void CopyPlaneToWorkingBuffer( /// <param name="height">The copied height in samples.</param>
Av1Plane plane, private static void CopyFilterOutput(
int subsamplingX, ReadOnlySpan<ushort> source,
int subsamplingY, int sourceStride,
int planeWidth, Span<byte> destination,
int planeHeight, int destinationOffset,
Span<ushort> destination) int destinationStride,
int width,
int height)
{ {
Span<byte> lowBitDepthPlane = default; for (int row = 0; row < height; row++)
Span<ushort> highBitDepthPlane = default;
int sourceStride;
if (this.frameBuffer.BytesPerSample == 2)
{ {
Span<short> signedPlane = this.frameBuffer.DeriveBlockPointer16( ReadOnlySpan<ushort> sourceRow = source.Slice(row * sourceStride, width);
plane, Span<byte> destinationRow = destination.Slice(destinationOffset + (row * destinationStride), width);
Point.Empty,
subsamplingX,
subsamplingY,
out sourceStride);
highBitDepthPlane = MemoryMarshal.Cast<short, ushort>(signedPlane);
}
else
{
lowBitDepthPlane = this.frameBuffer.DeriveBlockPointer(
plane,
Point.Empty,
subsamplingX,
subsamplingY,
out sourceStride);
}
for (int row = 0; row < planeHeight; row++) for (int column = 0; column < width; column++)
{
// DeriveBlockPointer spans begin on the row above the visible origin for prediction,
// hence the leading stride in every frame-relative row offset.
int frameOffset = sourceStride + (row * sourceStride);
Span<ushort> destinationRow = destination.Slice(row * planeWidth, planeWidth);
if (!highBitDepthPlane.IsEmpty)
{
highBitDepthPlane.Slice(frameOffset, planeWidth).CopyTo(destinationRow);
}
else
{ {
ReadOnlySpan<byte> sourceRow = lowBitDepthPlane.Slice(frameOffset, planeWidth); // The 8-bit restoration filters clip every result to the bit-depth range before this exact narrowing.
for (int column = 0; column < planeWidth; column++) destinationRow[column] = (byte)sourceRow[column];
{
destinationRow[column] = sourceRow[column];
}
} }
} }
} }
/// <summary> /// <summary>
/// Copies one restored 16-bit working buffer back to its reconstructed plane. /// Copies one native-width restoration buffer back to its reconstructed plane.
/// </summary> /// </summary>
/// <param name="plane">The luma or chroma plane.</param> /// <param name="plane">The luma or chroma plane.</param>
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param> /// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
/// <param name="subsamplingY">The vertical chroma subsampling shift.</param> /// <param name="subsamplingY">The vertical chroma subsampling shift.</param>
/// <param name="planeWidth">The visible plane width.</param> /// <param name="planeWidth">The visible plane width.</param>
/// <param name="planeHeight">The visible plane height.</param> /// <param name="planeHeight">The visible plane height.</param>
/// <param name="source">The row-major restored working buffer.</param> /// <param name="lowBitDepthSource">The row-major byte restoration buffer, when present.</param>
/// <param name="highBitDepthSource">The row-major native restoration buffer, when present.</param>
private void CopyWorkingBufferToPlane( private void CopyWorkingBufferToPlane(
Av1Plane plane, Av1Plane plane,
int subsamplingX, int subsamplingX,
int subsamplingY, int subsamplingY,
int planeWidth, int planeWidth,
int planeHeight, int planeHeight,
ReadOnlySpan<ushort> source) ReadOnlySpan<byte> lowBitDepthSource,
ReadOnlySpan<ushort> highBitDepthSource)
{ {
Span<byte> lowBitDepthPlane = default; Span<byte> lowBitDepthPlane = default;
Span<ushort> highBitDepthPlane = default; Span<ushort> highBitDepthPlane = default;
@ -544,18 +653,13 @@ internal class Av1LoopRestorationDecoder
{ {
// The destination view has the same preceding prediction row as the source view. // The destination view has the same preceding prediction row as the source view.
int frameOffset = destinationStride + (row * destinationStride); int frameOffset = destinationStride + (row * destinationStride);
ReadOnlySpan<ushort> sourceRow = source.Slice(row * planeWidth, planeWidth);
if (!highBitDepthPlane.IsEmpty) if (!highBitDepthPlane.IsEmpty)
{ {
sourceRow.CopyTo(highBitDepthPlane[frameOffset..]); highBitDepthSource.Slice(row * planeWidth, planeWidth).CopyTo(highBitDepthPlane[frameOffset..]);
} }
else else
{ {
Span<byte> destinationRow = lowBitDepthPlane.Slice(frameOffset, planeWidth); lowBitDepthSource.Slice(row * planeWidth, planeWidth).CopyTo(lowBitDepthPlane[frameOffset..]);
for (int column = 0; column < planeWidth; column++)
{
destinationRow[column] = (byte)sourceRow[column];
}
} }
} }
} }

10
src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1DeQuantizationContext.cs

@ -8,17 +8,17 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
/// <summary> /// <summary>
/// Stores the AV1 DC and AC dequantization values for every segment and color plane in a frame. /// Stores the AV1 DC and AC dequantization values for every segment and color plane in a frame.
/// </summary> /// </summary>
internal class Av1DeQuantizationContext internal sealed class Av1DeQuantizationContext
{ {
/// <summary> /// <summary>
/// The DC dequantization values indexed by segment and then plane. /// The DC dequantization values indexed by segment and then plane.
/// </summary> /// </summary>
private readonly short[][] dcContent; private InlineArray8<InlineArray4<short>> dcContent;
/// <summary> /// <summary>
/// The AC dequantization values indexed by segment and then plane. /// The AC dequantization values indexed by segment and then plane.
/// </summary> /// </summary>
private readonly short[][] acContent; private InlineArray8<InlineArray4<short>> acContent;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1DeQuantizationContext"/> class from the frame's base quantizer, /// Initializes a new instance of the <see cref="Av1DeQuantizationContext"/> class from the frame's base quantizer,
@ -29,12 +29,8 @@ internal class Av1DeQuantizationContext
public Av1DeQuantizationContext(ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) public Av1DeQuantizationContext(ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader)
{ {
Av1BitDepth bitDepth = sequenceHeader.ColorConfig.BitDepth; Av1BitDepth bitDepth = sequenceHeader.ColorConfig.BitDepth;
this.dcContent = new short[Av1Constants.MaxSegmentCount][];
this.acContent = new short[Av1Constants.MaxSegmentCount][];
for (int segmentId = 0; segmentId < Av1Constants.MaxSegmentCount; segmentId++) for (int segmentId = 0; segmentId < Av1Constants.MaxSegmentCount; segmentId++)
{ {
this.dcContent[segmentId] = new short[Av1Constants.MaxPlanes];
this.acContent[segmentId] = new short[Av1Constants.MaxPlanes];
int qindex = Av1QuantizationLookup.GetQIndex(frameHeader.SegmentationParameters, segmentId, frameHeader.QuantizationParameters.BaseQIndex); int qindex = Av1QuantizationLookup.GetQIndex(frameHeader.SegmentationParameters, segmentId, frameHeader.QuantizationParameters.BaseQIndex);
for (int plane = 0; plane < Av1Constants.MaxPlanes; plane++) for (int plane = 0; plane < Av1Constants.MaxPlanes; plane++)

2
src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizationLookup.cs

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
/// <summary> /// <summary>
/// Provides the normative AV1 inverse quantization matrices for each matrix level, plane class, and transform size. /// Provides the normative AV1 inverse quantization matrices for each matrix level, plane class, and transform size.
/// </summary> /// </summary>
internal class Av1InverseQuantizationLookup internal static class Av1InverseQuantizationLookup
{ {
/// <summary> /// <summary>
/// Maps each AV1 transform size to its stored matrix index; sizes with a 64-pixel dimension reuse the adjusted 32-pixel matrix. /// Maps each AV1 transform size to its stored matrix index; sizes with a 64-pixel dimension reuse the adjusted 32-pixel matrix.

4
src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizer.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
/// <summary> /// <summary>
/// Reconstructs AV1 transform coefficients from quantized coefficient levels. /// Reconstructs AV1 transform coefficients from quantized coefficient levels.
/// </summary> /// </summary>
internal class Av1InverseQuantizer internal sealed class Av1InverseQuantizer
{ {
/// <summary> /// <summary>
/// The sequence-level color configuration that determines coefficient precision. /// The sequence-level color configuration that determines coefficient precision.
@ -47,7 +47,6 @@ internal class Av1InverseQuantizer
public void UpdateDequant(Av1DeQuantizationContext deQuants, Av1SuperblockInfo superblockInfo) public void UpdateDequant(Av1DeQuantizationContext deQuants, Av1SuperblockInfo superblockInfo)
{ {
Av1BitDepth bitDepth = this.sequenceHeader.ColorConfig.BitDepth; Av1BitDepth bitDepth = this.sequenceHeader.ColorConfig.BitDepth;
Guard.NotNull(deQuants, nameof(deQuants));
this.deQuantsDeltaQ = deQuants; this.deQuantsDeltaQ = deQuants;
if (this.frameHeader.DeltaQParameters.IsPresent) if (this.frameHeader.DeltaQParameters.IsPresent)
{ {
@ -82,7 +81,6 @@ internal class Av1InverseQuantizer
/// <returns>The number of coefficient levels consumed.</returns> /// <returns>The number of coefficient levels consumed.</returns>
public int InverseQuantize(Av1BlockModeInfo mode, Span<int> level, Span<int> qCoefficients, Av1TransformType transformType, Av1TransformSize transformSize, Av1Plane plane) public int InverseQuantize(Av1BlockModeInfo mode, Span<int> level, Span<int> qCoefficients, Av1TransformType transformType, Av1TransformSize transformSize, Av1Plane plane)
{ {
Guard.NotNull(this.deQuantsDeltaQ);
Av1ScanOrder scanOrder = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType); Av1ScanOrder scanOrder = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType);
ReadOnlySpan<short> scanIndices = scanOrder.Scan; ReadOnlySpan<short> scanIndices = scanOrder.Scan;

7
src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1QuantizationLookup.cs

@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
/// <summary> /// <summary>
/// Provides the normative AV1 DC and AC dequantization values for each quantizer index and supported bit depth. /// Provides the normative AV1 DC and AC dequantization values for each quantizer index and supported bit depth.
/// </summary> /// </summary>
internal class Av1QuantizationLookup internal static class Av1QuantizationLookup
{ {
// Coefficient scaling and quantization with AV1 TX are tailored to // Coefficient scaling and quantization with AV1 TX are tailored to
// the AV1 TX transforms. Regardless of the bit-depth of the input, // the AV1 TX transforms. Regardless of the bit-depth of the input,
@ -223,7 +223,7 @@ internal class Av1QuantizationLookup
{ {
if (segmentationParameters.IsFeatureActive(segmentId, ObuSegmentationLevelFeature.AlternativeQuantizer)) if (segmentationParameters.IsFeatureActive(segmentId, ObuSegmentationLevelFeature.AlternativeQuantizer))
{ {
int data = segmentationParameters.FeatureData[segmentId, (int)ObuSegmentationLevelFeature.AlternativeQuantizer]; int data = segmentationParameters.GetFeatureData(segmentId, (int)ObuSegmentationLevelFeature.AlternativeQuantizer);
int qIndex = baseQIndex + data; int qIndex = baseQIndex + data;
return Av1Math.Clamp(qIndex, 0, Av1Constants.MaxQ); return Av1Math.Clamp(qIndex, 0, Av1Constants.MaxQ);
} }
@ -242,9 +242,6 @@ internal class Av1QuantizationLookup
ObuQuantizationParameters quantization = frameHeader.QuantizationParameters; ObuQuantizationParameters quantization = frameHeader.QuantizationParameters;
ObuSegmentationParameters segmentation = frameHeader.SegmentationParameters; ObuSegmentationParameters segmentation = frameHeader.SegmentationParameters;
frameHeader.CodedLossless = true; frameHeader.CodedLossless = true;
segmentation.QMLevel[0] = new int[Av1Constants.MaxSegmentCount];
segmentation.QMLevel[1] = new int[Av1Constants.MaxSegmentCount];
segmentation.QMLevel[2] = new int[Av1Constants.MaxSegmentCount];
for (int segmentId = 0; segmentId < Av1Constants.MaxSegmentCount; segmentId++) for (int segmentId = 0; segmentId < Av1Constants.MaxSegmentCount; segmentId++)
{ {
int qIndex = GetQIndex(segmentation, segmentId, quantization.BaseQIndex); int qIndex = GetQIndex(segmentation, segmentId, quantization.BaseQIndex);

2
src/ImageSharp/Formats/Heif/Av1/Pipeline/SuperResolution/Av1SuperResolutionDecoder.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.SuperResolution;
/// <summary> /// <summary>
/// Applies normative AV1 horizontal super-resolution upscaling to a reconstructed still-image frame. /// Applies normative AV1 horizontal super-resolution upscaling to a reconstructed still-image frame.
/// </summary> /// </summary>
internal class Av1SuperResolutionDecoder internal sealed class Av1SuperResolutionDecoder
{ {
/// <summary> /// <summary>
/// The sequence-level bit-depth and color-plane configuration. /// The sequence-level bit-depth and color-plane configuration.

136
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1BottomRightTopLeftConstants.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// Each bit describes whether a reference edge has already been reconstructed for one block in the AV1 partition traversal order. /// Each bit describes whether a reference edge has already been reconstructed for one block in the AV1 partition traversal order.
/// Separate tables preserve the alternate visit order used by mixed vertical partitions. /// Separate tables preserve the alternate visit order used by mixed vertical partitions.
/// </remarks> /// </remarks>
internal class Av1BottomRightTopLeftConstants internal static class Av1BottomRightTopLeftConstants
{ {
// Tables to store if the top-right reference pixels are available. The flags // Tables to store if the top-right reference pixels are available. The flags
// are represented with bits, packed into 8-bit integers. E.g., for the 32x32 // are represented with bits, packed into 8-bit integers. E.g., for the 32x32
@ -302,43 +302,6 @@ internal class Av1BottomRightTopLeftConstants
// There are tables for each of the square sizes. Vertical rectangles (like // There are tables for each of the square sizes. Vertical rectangles (like
// BLOCK_16X32) use their respective "non-vert" table // BLOCK_16X32) use their respective "non-vert" table
/// <summary>
/// Maps supported block-size values to top-right availability tables for mixed vertical partition traversal.
/// </summary>
/// <remarks>
/// Null entries identify block sizes that cannot use this traversal table; vertical rectangles reuse their standard tables.
/// </remarks>
private static readonly byte[]?[] HasTopRightVerticalTables = [
// 4X4
null,
// 4X8, 8X4, 8X8
HasTopRight4x8,
null,
HasTopRightVertical8x8,
// 8X16, 16X8, 16X16
HasTopRight8x16,
null,
HasTopRightVertical16x16,
// 16X32, 32X16, 32X32
HasTopRight16x32,
null,
HasTopRightVertical32x32,
// 32X64, 64X32, 64X64
HasTopRight32x64,
null,
HasTopRightVertical64x64,
// 64x128, 128x64, 128x128
HasTopRight64x128,
null,
HasTopRight128x128
];
// Similar to the has_tr_* tables, but store if the bottom-left reference // Similar to the has_tr_* tables, but store if the bottom-left reference
// pixels are available. // pixels are available.
@ -620,42 +583,6 @@ internal class Av1BottomRightTopLeftConstants
// There are tables for each of the square sizes. Vertical rectangles (like // There are tables for each of the square sizes. Vertical rectangles (like
// BLOCK_16X32) use their respective "non-vert" table // BLOCK_16X32) use their respective "non-vert" table
/// <summary>
/// Maps supported block-size values to bottom-left availability tables for mixed vertical partition traversal.
/// </summary>
/// <remarks>
/// Null entries identify block sizes that cannot use this traversal table; vertical rectangles reuse their standard tables.
/// </remarks>
private static readonly byte[]?[] HasBottomLeftVerticalTables = [
// 4X4
null,
// 4X8, 8X4, 8X8
HasBottomLeft4x8,
null,
HasBottomLeftVertical8x8,
// 8X16, 16X8, 16X16
HasBottomLeft8x16,
null,
HasBottomLeftVertical16x16,
// 16X32, 32X16, 32X32
HasBottomLeft16x32,
null,
HasBottomLeftVertical32x32,
// 32X64, 64X32, 64X64
HasBottomLeft32x64,
null,
HasBottomLeftVertical64x64,
// 64x128, 128x64, 128x128
HasBottomLeft64x128,
null,
HasBottomLeft128x128];
/// <summary> /// <summary>
/// Determines whether the top-right reference samples are available for a block at the specified traversal index. /// Determines whether the top-right reference samples are available for a block at the specified traversal index.
/// </summary> /// </summary>
@ -669,7 +596,7 @@ internal class Av1BottomRightTopLeftConstants
// remainder selects the bit within that byte. // remainder selects the bit within that byte.
int index1 = blockIndex / 8; int index1 = blockIndex / 8;
int index2 = blockIndex % 8; int index2 = blockIndex % 8;
Span<byte> hasTopRightTable = GetHasTopRightTable(partitionType, blockSize); ReadOnlySpan<byte> hasTopRightTable = GetHasTopRightTable(partitionType, blockSize);
return ((hasTopRightTable[index1] >> index2) & 1) > 0; return ((hasTopRightTable[index1] >> index2) & 1) > 0;
} }
@ -686,7 +613,7 @@ internal class Av1BottomRightTopLeftConstants
// remainder selects the bit within that byte. // remainder selects the bit within that byte.
int index1 = blockIndex / 8; int index1 = blockIndex / 8;
int index2 = blockIndex % 8; int index2 = blockIndex % 8;
Span<byte> hasBottomLeftTable = GetHasBottomLeftTable(partitionType, blockSize); ReadOnlySpan<byte> hasBottomLeftTable = GetHasBottomLeftTable(partitionType, blockSize);
return ((hasBottomLeftTable[index1] >> index2) & 1) > 0; return ((hasBottomLeftTable[index1] >> index2) & 1) > 0;
} }
@ -696,23 +623,30 @@ internal class Av1BottomRightTopLeftConstants
/// <param name="partition">The partition type that determines the block traversal order.</param> /// <param name="partition">The partition type that determines the block traversal order.</param>
/// <param name="blockSize">The block size whose availability table is selected.</param> /// <param name="blockSize">The block size whose availability table is selected.</param>
/// <returns>The packed top-right availability table.</returns> /// <returns>The packed top-right availability table.</returns>
private static Span<byte> GetHasTopRightTable(Av1PartitionType partition, Av1BlockSize blockSize) private static ReadOnlySpan<byte> GetHasTopRightTable(Av1PartitionType partition, Av1BlockSize blockSize)
{ {
byte[]? ret;
// If this is a mixed vertical partition, look up block size in vertical order. // If this is a mixed vertical partition, look up block size in vertical order.
if (partition is Av1PartitionType.VerticalA or Av1PartitionType.VerticalB) if (partition is Av1PartitionType.VerticalA or Av1PartitionType.VerticalB)
{ {
DebugGuard.MustBeLessThan((int)blockSize, (int)Av1BlockSize.SizeS, nameof(blockSize)); // libaom asserts that mixed-vertical traversal can select only vertical rectangles or squares.
ret = HasTopRightVerticalTables[(int)blockSize]; // Listing those shapes directly keeps the impossible horizontal-rectangle states out of the table type.
} return blockSize switch
else {
{ Av1BlockSize.Block4x8 => HasTopRight4x8,
ret = HasTopRightTables[(int)blockSize]; Av1BlockSize.Block8x8 => HasTopRightVertical8x8,
Av1BlockSize.Block8x16 => HasTopRight8x16,
Av1BlockSize.Block16x16 => HasTopRightVertical16x16,
Av1BlockSize.Block16x32 => HasTopRight16x32,
Av1BlockSize.Block32x32 => HasTopRightVertical32x32,
Av1BlockSize.Block32x64 => HasTopRight32x64,
Av1BlockSize.Block64x64 => HasTopRightVertical64x64,
Av1BlockSize.Block64x128 => HasTopRight64x128,
Av1BlockSize.Block128x128 => HasTopRight128x128,
_ => throw new InvalidOperationException("The mixed-vertical partition selected an invalid AV1 block size.")
};
} }
DebugGuard.NotNull(ret, nameof(ret)); return HasTopRightTables[(int)blockSize];
return ret;
} }
/// <summary> /// <summary>
@ -721,22 +655,28 @@ internal class Av1BottomRightTopLeftConstants
/// <param name="partition">The partition type that determines the block traversal order.</param> /// <param name="partition">The partition type that determines the block traversal order.</param>
/// <param name="blockSize">The block size whose availability table is selected.</param> /// <param name="blockSize">The block size whose availability table is selected.</param>
/// <returns>The packed bottom-left availability table.</returns> /// <returns>The packed bottom-left availability table.</returns>
private static Span<byte> GetHasBottomLeftTable(Av1PartitionType partition, Av1BlockSize blockSize) private static ReadOnlySpan<byte> GetHasBottomLeftTable(Av1PartitionType partition, Av1BlockSize blockSize)
{ {
byte[]? ret;
// If this is a mixed vertical partition, look up block size in vertical order. // If this is a mixed vertical partition, look up block size in vertical order.
if (partition is Av1PartitionType.VerticalA or Av1PartitionType.VerticalB) if (partition is Av1PartitionType.VerticalA or Av1PartitionType.VerticalB)
{ {
DebugGuard.MustBeLessThan((int)blockSize, (int)Av1BlockSize.SizeS, nameof(blockSize)); // The valid block shapes mirror the top-right table and the libaom traversal assertion.
ret = HasBottomLeftVerticalTables[(int)blockSize]; return blockSize switch
} {
else Av1BlockSize.Block4x8 => HasBottomLeft4x8,
{ Av1BlockSize.Block8x8 => HasBottomLeftVertical8x8,
ret = HasBottomLeftTables[(int)blockSize]; Av1BlockSize.Block8x16 => HasBottomLeft8x16,
Av1BlockSize.Block16x16 => HasBottomLeftVertical16x16,
Av1BlockSize.Block16x32 => HasBottomLeft16x32,
Av1BlockSize.Block32x32 => HasBottomLeftVertical32x32,
Av1BlockSize.Block32x64 => HasBottomLeft32x64,
Av1BlockSize.Block64x64 => HasBottomLeftVertical64x64,
Av1BlockSize.Block64x128 => HasBottomLeft64x128,
Av1BlockSize.Block128x128 => HasBottomLeft128x128,
_ => throw new InvalidOperationException("The mixed-vertical partition selected an invalid AV1 block size.")
};
} }
DebugGuard.NotNull(ret, nameof(ret)); return HasBottomLeftTables[(int)blockSize];
return ret;
} }
} }

8
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalIntraPredictor.Operations.cs

@ -575,7 +575,7 @@ internal static partial class Av1DirectionalIntraPredictor
else else
{ {
Av1PredictionMode mode = angle == 90 ? Av1PredictionMode.Vertical : Av1PredictionMode.Horizontal; Av1PredictionMode mode = angle == 90 ? Av1PredictionMode.Vertical : Av1PredictionMode.Horizontal;
Av1IntraPredictorBase.GetPredictor(mode).Predict(destination, destinationStride, above, left, width, height); Av1NonDirectionalIntraPredictorBase.GetPredictor(mode).Predict(destination, destinationStride, above, left, width, height);
} }
} }
@ -613,7 +613,7 @@ internal static partial class Av1DirectionalIntraPredictor
else else
{ {
Av1PredictionMode mode = angle == 90 ? Av1PredictionMode.Vertical : Av1PredictionMode.Horizontal; Av1PredictionMode mode = angle == 90 ? Av1PredictionMode.Vertical : Av1PredictionMode.Horizontal;
Av1IntraPredictorBase.GetPredictor(mode).Predict(destination, destinationStride, above, left, width, height); Av1NonDirectionalIntraPredictorBase.GetPredictor(mode).Predict(destination, destinationStride, above, left, width, height);
} }
} }
@ -648,7 +648,7 @@ internal static partial class Av1DirectionalIntraPredictor
else else
{ {
Av1PredictionMode mode = angle == 90 ? Av1PredictionMode.Vertical : Av1PredictionMode.Horizontal; Av1PredictionMode mode = angle == 90 ? Av1PredictionMode.Vertical : Av1PredictionMode.Horizontal;
Av1IntraPredictorBase.GetPredictor(mode).PredictScalar(destination, destinationStride, above, left, width, height); Av1NonDirectionalIntraPredictorBase.GetPredictor(mode).PredictScalar(destination, destinationStride, above, left, width, height);
} }
} }
@ -683,7 +683,7 @@ internal static partial class Av1DirectionalIntraPredictor
else else
{ {
Av1PredictionMode mode = angle == 90 ? Av1PredictionMode.Vertical : Av1PredictionMode.Horizontal; Av1PredictionMode mode = angle == 90 ? Av1PredictionMode.Vertical : Av1PredictionMode.Horizontal;
Av1IntraPredictorBase.GetPredictor(mode).PredictScalar(destination, destinationStride, above, left, width, height); Av1NonDirectionalIntraPredictorBase.GetPredictor(mode).PredictScalar(destination, destinationStride, above, left, width, height);
} }
} }

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.HorizontalOperator.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.HorizontalOperator.cs

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// <content> /// <content>
/// Provides horizontal intra prediction for scalar and SIMD sample representations. /// Provides horizontal intra prediction for scalar and SIMD sample representations.
/// </content> /// </content>
internal abstract partial class Av1IntraPredictorBase internal abstract partial class Av1NonDirectionalIntraPredictorBase
{ {
/// <summary> /// <summary>
/// Implements AV1 horizontal intra prediction for scalar and SIMD lanes. /// Implements AV1 horizontal intra prediction for scalar and SIMD lanes.

4
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.Operator.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.Operator.cs

@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// Defines the neighbor-usage flags and scalar/SIMD contract for closed intra-prediction operators, and provides /// Defines the neighbor-usage flags and scalar/SIMD contract for closed intra-prediction operators, and provides
/// their shared width-progressive SIMD traversal. /// their shared width-progressive SIMD traversal.
/// </content> /// </content>
internal abstract partial class Av1IntraPredictorBase internal abstract partial class Av1NonDirectionalIntraPredictorBase
{ {
/// <summary> /// <summary>
/// Identifies the neighboring inputs consumed by an AV1 intra-prediction operator. /// Identifies the neighboring inputs consumed by an AV1 intra-prediction operator.
@ -194,7 +194,7 @@ internal abstract partial class Av1IntraPredictorBase
/// left sample and row weight are broadcast. <typeparamref name="TOperator"/> declares which references it uses; /// left sample and row weight are broadcast. <typeparamref name="TOperator"/> declares which references it uses;
/// because the operator type is closed, the JIT can remove unused loads and broadcasts from each prediction mode. /// because the operator type is closed, the JIT can remove unused loads and broadcasts from each prediction mode.
/// </remarks> /// </remarks>
internal sealed class Av1IntraPredictor<TOperator> : Av1IntraPredictorBase internal sealed class Av1NonDirectionalIntraPredictor<TOperator> : Av1NonDirectionalIntraPredictorBase
where TOperator : struct, IAv1IntraPredictionOperator where TOperator : struct, IAv1IntraPredictionOperator
{ {
/// <inheritdoc/> /// <inheritdoc/>

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.Packing.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.Packing.cs

@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// perform interpolation in signed 32-bit lanes and clip before narrowing, so the unsigned byte overloads may use /// perform interpolation in signed 32-bit lanes and clip before narrowing, so the unsigned byte overloads may use
/// unsigned narrowing and the high-bit-depth overloads may use signed narrowing without additional saturation logic. /// unsigned narrowing and the high-bit-depth overloads may use signed narrowing without additional saturation logic.
/// </remarks> /// </remarks>
internal abstract partial class Av1IntraPredictorBase internal abstract partial class Av1NonDirectionalIntraPredictorBase
{ {
/// <summary> /// <summary>
/// Widens sixteen 8-bit samples into four 32-bit vectors. /// Widens sixteen 8-bit samples into four 32-bit vectors.

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.PaethOperator.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.PaethOperator.cs

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// <content> /// <content>
/// Provides Paeth intra prediction for scalar and SIMD sample representations. /// Provides Paeth intra prediction for scalar and SIMD sample representations.
/// </content> /// </content>
internal abstract partial class Av1IntraPredictorBase internal abstract partial class Av1NonDirectionalIntraPredictorBase
{ {
/// <summary> /// <summary>
/// Implements AV1 Paeth intra prediction for scalar and SIMD lanes. /// Implements AV1 Paeth intra prediction for scalar and SIMD lanes.

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.SmoothHorizontalOperator.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothHorizontalOperator.cs

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// <content> /// <content>
/// Provides horizontal smooth intra prediction for scalar and SIMD sample representations. /// Provides horizontal smooth intra prediction for scalar and SIMD sample representations.
/// </content> /// </content>
internal abstract partial class Av1IntraPredictorBase internal abstract partial class Av1NonDirectionalIntraPredictorBase
{ {
/// <summary> /// <summary>
/// Implements horizontal AV1 smooth intra prediction for scalar and SIMD lanes. /// Implements horizontal AV1 smooth intra prediction for scalar and SIMD lanes.

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.SmoothOperator.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothOperator.cs

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// <content> /// <content>
/// Provides two-dimensional smooth intra prediction for scalar and SIMD sample representations. /// Provides two-dimensional smooth intra prediction for scalar and SIMD sample representations.
/// </content> /// </content>
internal abstract partial class Av1IntraPredictorBase internal abstract partial class Av1NonDirectionalIntraPredictorBase
{ {
/// <summary> /// <summary>
/// Implements two-dimensional AV1 smooth intra prediction for scalar and SIMD lanes. /// Implements two-dimensional AV1 smooth intra prediction for scalar and SIMD lanes.

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.SmoothVerticalOperator.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothVerticalOperator.cs

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// <content> /// <content>
/// Provides vertical smooth intra prediction for scalar and SIMD sample representations. /// Provides vertical smooth intra prediction for scalar and SIMD sample representations.
/// </content> /// </content>
internal abstract partial class Av1IntraPredictorBase internal abstract partial class Av1NonDirectionalIntraPredictorBase
{ {
/// <summary> /// <summary>
/// Implements vertical AV1 smooth intra prediction for scalar and SIMD lanes. /// Implements vertical AV1 smooth intra prediction for scalar and SIMD lanes.

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.VerticalOperator.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.VerticalOperator.cs

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// <content> /// <content>
/// Provides vertical intra prediction for scalar and SIMD sample representations. /// Provides vertical intra prediction for scalar and SIMD sample representations.
/// </content> /// </content>
internal abstract partial class Av1IntraPredictorBase internal abstract partial class Av1NonDirectionalIntraPredictorBase
{ {
/// <summary> /// <summary>
/// Implements AV1 vertical intra prediction for scalar and SIMD lanes. /// Implements AV1 vertical intra prediction for scalar and SIMD lanes.

16
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictorBase.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictorBase.cs

@ -11,37 +11,37 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// <remarks> /// <remarks>
/// The implementation covers the non-directional prediction processes in section 7.11.2 of the AV1 specification. /// The implementation covers the non-directional prediction processes in section 7.11.2 of the AV1 specification.
/// </remarks> /// </remarks>
internal abstract partial class Av1IntraPredictorBase internal abstract partial class Av1NonDirectionalIntraPredictorBase
{ {
/// <summary> /// <summary>
/// The horizontal prediction operator. /// The horizontal prediction operator.
/// </summary> /// </summary>
private static readonly Av1IntraPredictor<HorizontalOperator> HorizontalPredictor = new(); private static readonly Av1NonDirectionalIntraPredictor<HorizontalOperator> HorizontalPredictor = new();
/// <summary> /// <summary>
/// The vertical prediction operator. /// The vertical prediction operator.
/// </summary> /// </summary>
private static readonly Av1IntraPredictor<VerticalOperator> VerticalPredictor = new(); private static readonly Av1NonDirectionalIntraPredictor<VerticalOperator> VerticalPredictor = new();
/// <summary> /// <summary>
/// The Paeth prediction operator. /// The Paeth prediction operator.
/// </summary> /// </summary>
private static readonly Av1IntraPredictor<PaethOperator> PaethPredictor = new(); private static readonly Av1NonDirectionalIntraPredictor<PaethOperator> PaethPredictor = new();
/// <summary> /// <summary>
/// The two-dimensional smooth prediction operator. /// The two-dimensional smooth prediction operator.
/// </summary> /// </summary>
private static readonly Av1IntraPredictor<SmoothOperator> SmoothPredictor = new(); private static readonly Av1NonDirectionalIntraPredictor<SmoothOperator> SmoothPredictor = new();
/// <summary> /// <summary>
/// The horizontal smooth prediction operator. /// The horizontal smooth prediction operator.
/// </summary> /// </summary>
private static readonly Av1IntraPredictor<SmoothHorizontalOperator> SmoothHorizontalPredictor = new(); private static readonly Av1NonDirectionalIntraPredictor<SmoothHorizontalOperator> SmoothHorizontalPredictor = new();
/// <summary> /// <summary>
/// The vertical smooth prediction operator. /// The vertical smooth prediction operator.
/// </summary> /// </summary>
private static readonly Av1IntraPredictor<SmoothVerticalOperator> SmoothVerticalPredictor = new(); private static readonly Av1NonDirectionalIntraPredictor<SmoothVerticalOperator> SmoothVerticalPredictor = new();
/// <summary> /// <summary>
/// Gets the Q8 smooth weights for every supported block dimension. /// Gets the Q8 smooth weights for every supported block dimension.
@ -73,7 +73,7 @@ internal abstract partial class Av1IntraPredictorBase
/// </summary> /// </summary>
/// <param name="mode">The decoded non-directional prediction mode.</param> /// <param name="mode">The decoded non-directional prediction mode.</param>
/// <returns>The predictor for <paramref name="mode"/>.</returns> /// <returns>The predictor for <paramref name="mode"/>.</returns>
public static Av1IntraPredictorBase GetPredictor(Av1PredictionMode mode) public static Av1NonDirectionalIntraPredictorBase GetPredictor(Av1PredictionMode mode)
=> mode switch => mode switch
{ {
Av1PredictionMode.Horizontal => HorizontalPredictor, Av1PredictionMode.Horizontal => HorizontalPredictor,

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

@ -23,7 +23,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// to adjacent SIMD lanes, exact-width stores interleave filtered half samples with the original edge, and scalar /// to adjacent SIMD lanes, exact-width stores interleave filtered half samples with the original edge, and scalar
/// continuations handle only incomplete vectors. The completed edges then feed the closed prediction operators. /// continuations handle only incomplete vectors. The completed edges then feed the closed prediction operators.
/// </remarks> /// </remarks>
internal class Av1PredictionDecoder internal sealed class Av1PredictionDecoder
{ {
/// <summary> /// <summary>
/// The largest edge length for which AV1 permits intra-edge upsampling. /// The largest edge length for which AV1 permits intra-edge upsampling.
@ -60,17 +60,28 @@ internal class Av1PredictionDecoder
/// </summary> /// </summary>
private readonly Memory<short> predictorScratch; private readonly Memory<short> predictorScratch;
/// <summary>
/// The complete decoder-session palette color-index map state, when supplied by a decoder session.
/// </summary>
private readonly Av1TileReader.PaletteColorIndexMaps? paletteColorIndexMaps;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1PredictionDecoder"/> class. /// Initializes a new instance of the <see cref="Av1PredictionDecoder"/> class.
/// </summary> /// </summary>
/// <param name="sequenceHeader">The decoded sequence header for the current image.</param> /// <param name="sequenceHeader">The decoded sequence header for the current image.</param>
/// <param name="frameHeader">The decoded frame header for the current image.</param> /// <param name="frameHeader">The decoded frame header for the current image.</param>
/// <param name="predictorScratch">The reusable predictor workspace owned by the containing block decoder.</param> /// <param name="predictorScratch">The reusable predictor workspace owned by the containing block decoder.</param>
public Av1PredictionDecoder(ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, Memory<short> predictorScratch) /// <param name="paletteColorIndexMaps">The complete decoder-session palette map state.</param>
public Av1PredictionDecoder(
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader,
Memory<short> predictorScratch,
Av1TileReader.PaletteColorIndexMaps? paletteColorIndexMaps = null)
{ {
this.sequenceHeader = sequenceHeader; this.sequenceHeader = sequenceHeader;
this.frameHeader = frameHeader; this.frameHeader = frameHeader;
this.predictorScratch = predictorScratch; this.predictorScratch = predictorScratch;
this.paletteColorIndexMaps = paletteColorIndexMaps;
} }
/// <summary> /// <summary>
@ -462,7 +473,18 @@ internal class Av1PredictionDecoder
if (usePalette) if (usePalette)
{ {
ReadOnlySpan<ushort> paletteColors = modeInfo.GetPaletteColors(plane); ReadOnlySpan<ushort> paletteColors = modeInfo.GetPaletteColors(plane);
Buffer2DRegion<byte> colorIndexMap = modeInfo.GetPaletteColorIndexMap(plane); Av1TileReader.PaletteColorIndexMaps? paletteColorIndexMapState = this.paletteColorIndexMaps;
if (paletteColorIndexMapState is null)
{
throw new InvalidOperationException("Palette prediction requires decoder-session color-index maps.");
}
Av1TileReader.PaletteColorIndexMaps paletteColorIndexMaps = paletteColorIndexMapState.Value;
Buffer2D<byte> colorIndexBuffer = plane == Av1Plane.Y
? paletteColorIndexMaps.Luma
: paletteColorIndexMaps.Chroma;
Buffer2DRegion<byte> colorIndexMap = modeInfo.GetPaletteColorIndexMap(plane, colorIndexBuffer);
Buffer2DRegion<byte> transformColorIndexMap = colorIndexMap.GetSubRegion( Buffer2DRegion<byte> transformColorIndexMap = colorIndexMap.GetSubRegion(
blockModeInfoColumnOffset << Av1Constants.ModeInfoSizeLog2, blockModeInfoColumnOffset << Av1Constants.ModeInfoSizeLog2,
blockModeInfoRowOffset << Av1Constants.ModeInfoSizeLog2, blockModeInfoRowOffset << Av1Constants.ModeInfoSizeLog2,
@ -1215,7 +1237,7 @@ internal class Av1PredictionDecoder
{ {
int width = transformSize.GetWidth(); int width = transformSize.GetWidth();
int height = transformSize.GetHeight(); int height = transformSize.GetHeight();
Av1IntraPredictorBase predictor = Av1IntraPredictorBase.GetPredictor(mode); Av1NonDirectionalIntraPredictorBase predictor = Av1NonDirectionalIntraPredictorBase.GetPredictor(mode);
if (typeof(T) == typeof(byte)) if (typeof(T) == typeof(byte))
{ {
@ -2004,7 +2026,7 @@ internal class Av1PredictionDecoder
} }
else else
{ {
// Inter mode not supported here. // Chroma modes use their own enum and carry only the intra predictors relevant to this neighbor check.
Av1ChromaPredictionMode uvMode = modeInfo.UvMode; Av1ChromaPredictionMode uvMode = modeInfo.UvMode;
return uvMode is Av1ChromaPredictionMode.Smooth or return uvMode is Av1ChromaPredictionMode.Smooth or
Av1ChromaPredictionMode.SmoothVertical or Av1ChromaPredictionMode.SmoothVertical or

6
src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.Operations.cs

@ -29,7 +29,7 @@ internal partial class Av1ChromaFromLumaContext
private void StoreSamples(ReadOnlySpan<byte> input, int inputStride, int outputOffset, int width, int height) private void StoreSamples(ReadOnlySpan<byte> input, int inputStride, int outputOffset, int width, int height)
{ {
ref byte inputBase = ref MemoryMarshal.GetReference(input); ref byte inputBase = ref MemoryMarshal.GetReference(input);
ref short outputBase = ref MemoryMarshal.GetArrayDataReference(this.Q3Buffer); ref short outputBase = ref MemoryMarshal.GetReference(this.Q3Buffer);
if (!this.subX) if (!this.subX)
{ {
@ -181,7 +181,7 @@ internal partial class Av1ChromaFromLumaContext
private void StoreSamples(ReadOnlySpan<short> input, int inputStride, int outputOffset, int width, int height) private void StoreSamples(ReadOnlySpan<short> input, int inputStride, int outputOffset, int width, int height)
{ {
ref short inputBase = ref MemoryMarshal.GetReference(input); ref short inputBase = ref MemoryMarshal.GetReference(input);
ref short outputBase = ref MemoryMarshal.GetArrayDataReference(this.Q3Buffer); ref short outputBase = ref MemoryMarshal.GetReference(this.Q3Buffer);
if (!this.subX) if (!this.subX)
{ {
@ -305,7 +305,7 @@ internal partial class Av1ChromaFromLumaContext
// Transform dimensions are powers of two, so division by the sample count is an exact right shift. Half // Transform dimensions are powers of two, so division by the sample count is an exact right shift. Half
// the sample count is accumulated first to implement the normative nearest-integer rounding. // the sample count is accumulated first to implement the normative nearest-integer rounding.
int sumQ3 = (width * height) >> 1; int sumQ3 = (width * height) >> 1;
ref short bufferBase = ref MemoryMarshal.GetArrayDataReference(this.Q3Buffer); ref short bufferBase = ref MemoryMarshal.GetReference(this.Q3Buffer);
if (Vector256.IsHardwareAccelerated && width >= Vector256<short>.Count) if (Vector256.IsHardwareAccelerated && width >= Vector256<short>.Count)
{ {

33
src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.cs

@ -11,13 +11,23 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
/// <summary> /// <summary>
/// Accumulates subsampled luma samples and derives the zero-mean Q3 predictor surface used by AV1 chroma-from-luma prediction. /// Accumulates subsampled luma samples and derives the zero-mean Q3 predictor surface used by AV1 chroma-from-luma prediction.
/// </summary> /// </summary>
internal partial class Av1ChromaFromLumaContext internal sealed partial class Av1ChromaFromLumaContext
{ {
/// <summary> /// <summary>
/// The fixed row stride and maximum dimension, in chroma samples, of the luma predictor buffer. /// The fixed row stride and maximum dimension, in chroma samples, of the luma predictor buffer.
/// </summary> /// </summary>
private const int BufferLine = 32; private const int BufferLine = 32;
/// <summary>
/// The number of samples in the fixed-stride chroma-from-luma workspace.
/// </summary>
public const int BufferLength = BufferLine * BufferLine;
/// <summary>
/// The caller-owned fixed-stride luma predictor workspace.
/// </summary>
private readonly Memory<short> q3Buffer;
/// <summary> /// <summary>
/// The number of initialized predictor rows currently stored in <see cref="Q3Buffer"/>. /// The number of initialized predictor rows currently stored in <see cref="Q3Buffer"/>.
/// </summary> /// </summary>
@ -43,16 +53,26 @@ internal partial class Av1ChromaFromLumaContext
/// </summary> /// </summary>
/// <param name="colorConfig">The AV1 color configuration that supplies chroma subsampling.</param> /// <param name="colorConfig">The AV1 color configuration that supplies chroma subsampling.</param>
public Av1ChromaFromLumaContext(ObuColorConfig colorConfig) public Av1ChromaFromLumaContext(ObuColorConfig colorConfig)
: this(colorConfig, new short[BufferLength])
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1ChromaFromLumaContext"/> class over caller-owned workspace.
/// </summary>
/// <param name="colorConfig">The AV1 color configuration that supplies chroma subsampling.</param>
/// <param name="q3Buffer">The fixed-stride signed Q3 workspace retained for the context lifetime.</param>
public Av1ChromaFromLumaContext(ObuColorConfig colorConfig, Memory<short> q3Buffer)
{ {
this.subX = colorConfig.SubSamplingX; this.subX = colorConfig.SubSamplingX;
this.subY = colorConfig.SubSamplingY; this.subY = colorConfig.SubSamplingY;
this.Q3Buffer = new short[BufferLine * BufferLine]; this.q3Buffer = q3Buffer;
} }
/// <summary> /// <summary>
/// Gets the fixed-stride luma predictor samples in signed Q3 fixed-point representation. /// Gets the fixed-stride luma predictor samples in signed Q3 fixed-point representation.
/// </summary> /// </summary>
public short[] Q3Buffer { get; } public Span<short> Q3Buffer => this.q3Buffer.Span;
/// <summary> /// <summary>
/// Gets a value indicating whether edge padding and mean subtraction have been applied to the current samples. /// Gets a value indicating whether edge padding and mean subtraction have been applied to the current samples.
@ -154,6 +174,7 @@ internal partial class Av1ChromaFromLumaContext
{ {
int differenceWidth = width - this.bufferWidth; int differenceWidth = width - this.bufferWidth;
int differenceHeight = height - this.bufferHeight; int differenceHeight = height - this.bufferHeight;
Span<short> q3Buffer = this.Q3Buffer;
if (differenceWidth > 0) if (differenceWidth > 0)
{ {
@ -163,8 +184,8 @@ internal partial class Av1ChromaFromLumaContext
for (int y = 0; y < minimumHeight; y++) for (int y = 0; y < minimumHeight; y++)
{ {
int rowOffset = y * BufferLine; int rowOffset = y * BufferLine;
short lastPixel = this.Q3Buffer[rowOffset + this.bufferWidth - 1]; short lastPixel = q3Buffer[rowOffset + this.bufferWidth - 1];
this.Q3Buffer.AsSpan(rowOffset + this.bufferWidth, differenceWidth).Fill(lastPixel); q3Buffer.Slice(rowOffset + this.bufferWidth, differenceWidth).Fill(lastPixel);
} }
this.bufferWidth = width; this.bufferWidth = width;
@ -176,7 +197,7 @@ internal partial class Av1ChromaFromLumaContext
for (int y = this.bufferHeight; y < height; y++) for (int y = this.bufferHeight; y < height; y++)
{ {
int rowOffset = y * BufferLine; int rowOffset = y * BufferLine;
this.Q3Buffer.AsSpan(rowOffset - BufferLine, width).CopyTo(this.Q3Buffer.AsSpan(rowOffset, width)); q3Buffer.Slice(rowOffset - BufferLine, width).CopyTo(q3Buffer.Slice(rowOffset, width));
} }
this.bufferHeight = height; this.bufferHeight = height;

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundAveragePredictor.Operator.cs

@ -4,7 +4,7 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

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

@ -4,7 +4,7 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

38
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeightedPredictor.Operator.cs

@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
@ -119,9 +119,9 @@ internal static partial class Av1CompoundDistanceWeightedPredictor
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Blend(Vector128<byte> first, Vector128<byte> second, int firstWeight, int secondWeight) public static Vector128<byte> Blend(Vector128<byte> first, Vector128<byte> second, int firstWeight, int secondWeight)
{ {
Av1IntraPredictorBase.Widen(first, out Vector128<int> first0, out Vector128<int> first1, out Vector128<int> first2, out Vector128<int> first3); Av1NonDirectionalIntraPredictorBase.Widen(first, out Vector128<int> first0, out Vector128<int> first1, out Vector128<int> first2, out Vector128<int> first3);
Av1IntraPredictorBase.Widen(second, out Vector128<int> second0, out Vector128<int> second1, out Vector128<int> second2, out Vector128<int> second3); Av1NonDirectionalIntraPredictorBase.Widen(second, out Vector128<int> second0, out Vector128<int> second1, out Vector128<int> second2, out Vector128<int> second3);
return Av1IntraPredictorBase.Narrow( return Av1NonDirectionalIntraPredictorBase.Narrow(
Blend(first0, second0, firstWeight, secondWeight), Blend(first0, second0, firstWeight, secondWeight),
Blend(first1, second1, firstWeight, secondWeight), Blend(first1, second1, firstWeight, secondWeight),
Blend(first2, second2, firstWeight, secondWeight), Blend(first2, second2, firstWeight, secondWeight),
@ -132,9 +132,9 @@ internal static partial class Av1CompoundDistanceWeightedPredictor
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<byte> Blend(Vector256<byte> first, Vector256<byte> second, int firstWeight, int secondWeight) public static Vector256<byte> Blend(Vector256<byte> first, Vector256<byte> second, int firstWeight, int secondWeight)
{ {
Av1IntraPredictorBase.Widen(first, out Vector256<int> first0, out Vector256<int> first1, out Vector256<int> first2, out Vector256<int> first3); Av1NonDirectionalIntraPredictorBase.Widen(first, out Vector256<int> first0, out Vector256<int> first1, out Vector256<int> first2, out Vector256<int> first3);
Av1IntraPredictorBase.Widen(second, out Vector256<int> second0, out Vector256<int> second1, out Vector256<int> second2, out Vector256<int> second3); Av1NonDirectionalIntraPredictorBase.Widen(second, out Vector256<int> second0, out Vector256<int> second1, out Vector256<int> second2, out Vector256<int> second3);
return Av1IntraPredictorBase.Narrow( return Av1NonDirectionalIntraPredictorBase.Narrow(
Blend(first0, second0, firstWeight, secondWeight), Blend(first0, second0, firstWeight, secondWeight),
Blend(first1, second1, firstWeight, secondWeight), Blend(first1, second1, firstWeight, secondWeight),
Blend(first2, second2, firstWeight, secondWeight), Blend(first2, second2, firstWeight, secondWeight),
@ -145,9 +145,9 @@ internal static partial class Av1CompoundDistanceWeightedPredictor
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<byte> Blend(Vector512<byte> first, Vector512<byte> second, int firstWeight, int secondWeight) public static Vector512<byte> Blend(Vector512<byte> first, Vector512<byte> second, int firstWeight, int secondWeight)
{ {
Av1IntraPredictorBase.Widen(first, out Vector512<int> first0, out Vector512<int> first1, out Vector512<int> first2, out Vector512<int> first3); Av1NonDirectionalIntraPredictorBase.Widen(first, out Vector512<int> first0, out Vector512<int> first1, out Vector512<int> first2, out Vector512<int> first3);
Av1IntraPredictorBase.Widen(second, out Vector512<int> second0, out Vector512<int> second1, out Vector512<int> second2, out Vector512<int> second3); Av1NonDirectionalIntraPredictorBase.Widen(second, out Vector512<int> second0, out Vector512<int> second1, out Vector512<int> second2, out Vector512<int> second3);
return Av1IntraPredictorBase.Narrow( return Av1NonDirectionalIntraPredictorBase.Narrow(
Blend(first0, second0, firstWeight, secondWeight), Blend(first0, second0, firstWeight, secondWeight),
Blend(first1, second1, firstWeight, secondWeight), Blend(first1, second1, firstWeight, secondWeight),
Blend(first2, second2, firstWeight, secondWeight), Blend(first2, second2, firstWeight, secondWeight),
@ -158,9 +158,9 @@ internal static partial class Av1CompoundDistanceWeightedPredictor
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<ushort> Blend(Vector128<ushort> first, Vector128<ushort> second, int firstWeight, int secondWeight) public static Vector128<ushort> Blend(Vector128<ushort> first, Vector128<ushort> second, int firstWeight, int secondWeight)
{ {
Av1IntraPredictorBase.Widen(first.AsInt16(), out Vector128<int> first0, out Vector128<int> first1); Av1NonDirectionalIntraPredictorBase.Widen(first.AsInt16(), out Vector128<int> first0, out Vector128<int> first1);
Av1IntraPredictorBase.Widen(second.AsInt16(), out Vector128<int> second0, out Vector128<int> second1); Av1NonDirectionalIntraPredictorBase.Widen(second.AsInt16(), out Vector128<int> second0, out Vector128<int> second1);
return Av1IntraPredictorBase.Narrow( return Av1NonDirectionalIntraPredictorBase.Narrow(
Blend(first0, second0, firstWeight, secondWeight), Blend(first0, second0, firstWeight, secondWeight),
Blend(first1, second1, firstWeight, secondWeight)).AsUInt16(); Blend(first1, second1, firstWeight, secondWeight)).AsUInt16();
} }
@ -169,9 +169,9 @@ internal static partial class Av1CompoundDistanceWeightedPredictor
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<ushort> Blend(Vector256<ushort> first, Vector256<ushort> second, int firstWeight, int secondWeight) public static Vector256<ushort> Blend(Vector256<ushort> first, Vector256<ushort> second, int firstWeight, int secondWeight)
{ {
Av1IntraPredictorBase.Widen(first.AsInt16(), out Vector256<int> first0, out Vector256<int> first1); Av1NonDirectionalIntraPredictorBase.Widen(first.AsInt16(), out Vector256<int> first0, out Vector256<int> first1);
Av1IntraPredictorBase.Widen(second.AsInt16(), out Vector256<int> second0, out Vector256<int> second1); Av1NonDirectionalIntraPredictorBase.Widen(second.AsInt16(), out Vector256<int> second0, out Vector256<int> second1);
return Av1IntraPredictorBase.Narrow( return Av1NonDirectionalIntraPredictorBase.Narrow(
Blend(first0, second0, firstWeight, secondWeight), Blend(first0, second0, firstWeight, secondWeight),
Blend(first1, second1, firstWeight, secondWeight)).AsUInt16(); Blend(first1, second1, firstWeight, secondWeight)).AsUInt16();
} }
@ -180,9 +180,9 @@ internal static partial class Av1CompoundDistanceWeightedPredictor
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<ushort> Blend(Vector512<ushort> first, Vector512<ushort> second, int firstWeight, int secondWeight) public static Vector512<ushort> Blend(Vector512<ushort> first, Vector512<ushort> second, int firstWeight, int secondWeight)
{ {
Av1IntraPredictorBase.Widen(first.AsInt16(), out Vector512<int> first0, out Vector512<int> first1); Av1NonDirectionalIntraPredictorBase.Widen(first.AsInt16(), out Vector512<int> first0, out Vector512<int> first1);
Av1IntraPredictorBase.Widen(second.AsInt16(), out Vector512<int> second0, out Vector512<int> second1); Av1NonDirectionalIntraPredictorBase.Widen(second.AsInt16(), out Vector512<int> second0, out Vector512<int> second1);
return Av1IntraPredictorBase.Narrow( return Av1NonDirectionalIntraPredictorBase.Narrow(
Blend(first0, second0, firstWeight, secondWeight), Blend(first0, second0, firstWeight, secondWeight),
Blend(first1, second1, firstWeight, secondWeight)).AsUInt16(); Blend(first1, second1, firstWeight, secondWeight)).AsUInt16();
} }

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

@ -6,7 +6,7 @@ using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

32
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Operator.cs

@ -4,7 +4,7 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
@ -430,7 +430,7 @@ internal static partial class Av1CompoundInterPredictor
int preShift, int preShift,
int round, int round,
int roundOffset) int roundOffset)
=> Av1IntraPredictorBase.Narrow( => Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(lower << preShift, round) + Vector128.Create(roundOffset), RoundPowerOfTwo(lower << preShift, round) + Vector128.Create(roundOffset),
RoundPowerOfTwo(upper << preShift, round) + Vector128.Create(roundOffset)).AsUInt16(); RoundPowerOfTwo(upper << preShift, round) + Vector128.Create(roundOffset)).AsUInt16();
@ -442,7 +442,7 @@ internal static partial class Av1CompoundInterPredictor
int preShift, int preShift,
int round, int round,
int roundOffset) int roundOffset)
=> Av1IntraPredictorBase.Narrow( => Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(lower << preShift, round) + Vector256.Create(roundOffset), RoundPowerOfTwo(lower << preShift, round) + Vector256.Create(roundOffset),
RoundPowerOfTwo(upper << preShift, round) + Vector256.Create(roundOffset)).AsUInt16(); RoundPowerOfTwo(upper << preShift, round) + Vector256.Create(roundOffset)).AsUInt16();
@ -454,7 +454,7 @@ internal static partial class Av1CompoundInterPredictor
int preShift, int preShift,
int round, int round,
int roundOffset) int roundOffset)
=> Av1IntraPredictorBase.Narrow( => Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(lower << preShift, round) + Vector512.Create(roundOffset), RoundPowerOfTwo(lower << preShift, round) + Vector512.Create(roundOffset),
RoundPowerOfTwo(upper << preShift, round) + Vector512.Create(roundOffset)).AsUInt16(); RoundPowerOfTwo(upper << preShift, round) + Vector512.Create(roundOffset)).AsUInt16();
@ -466,21 +466,21 @@ internal static partial class Av1CompoundInterPredictor
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<short> PrepareHorizontal(Vector128<int> lower, Vector128<int> upper) public static Vector128<short> PrepareHorizontal(Vector128<int> lower, Vector128<int> upper)
=> Av1IntraPredictorBase.Narrow( => Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector128.Create(HorizontalBias), Round0Bits), RoundPowerOfTwo(lower + Vector128.Create(HorizontalBias), Round0Bits),
RoundPowerOfTwo(upper + Vector128.Create(HorizontalBias), Round0Bits)); RoundPowerOfTwo(upper + Vector128.Create(HorizontalBias), Round0Bits));
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<short> PrepareHorizontal(Vector256<int> lower, Vector256<int> upper) public static Vector256<short> PrepareHorizontal(Vector256<int> lower, Vector256<int> upper)
=> Av1IntraPredictorBase.Narrow( => Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector256.Create(HorizontalBias), Round0Bits), RoundPowerOfTwo(lower + Vector256.Create(HorizontalBias), Round0Bits),
RoundPowerOfTwo(upper + Vector256.Create(HorizontalBias), Round0Bits)); RoundPowerOfTwo(upper + Vector256.Create(HorizontalBias), Round0Bits));
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<short> PrepareHorizontal(Vector512<int> lower, Vector512<int> upper) public static Vector512<short> PrepareHorizontal(Vector512<int> lower, Vector512<int> upper)
=> Av1IntraPredictorBase.Narrow( => Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector512.Create(HorizontalBias), Round0Bits), RoundPowerOfTwo(lower + Vector512.Create(HorizontalBias), Round0Bits),
RoundPowerOfTwo(upper + Vector512.Create(HorizontalBias), Round0Bits)); RoundPowerOfTwo(upper + Vector512.Create(HorizontalBias), Round0Bits));
@ -496,7 +496,7 @@ internal static partial class Av1CompoundInterPredictor
Vector128<int> upper, Vector128<int> upper,
int bias, int bias,
int round) int round)
=> Av1IntraPredictorBase.Narrow( => Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector128.Create(bias), round), RoundPowerOfTwo(lower + Vector128.Create(bias), round),
RoundPowerOfTwo(upper + Vector128.Create(bias), round)); RoundPowerOfTwo(upper + Vector128.Create(bias), round));
@ -507,7 +507,7 @@ internal static partial class Av1CompoundInterPredictor
Vector256<int> upper, Vector256<int> upper,
int bias, int bias,
int round) int round)
=> Av1IntraPredictorBase.Narrow( => Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector256.Create(bias), round), RoundPowerOfTwo(lower + Vector256.Create(bias), round),
RoundPowerOfTwo(upper + Vector256.Create(bias), round)); RoundPowerOfTwo(upper + Vector256.Create(bias), round));
@ -518,7 +518,7 @@ internal static partial class Av1CompoundInterPredictor
Vector512<int> upper, Vector512<int> upper,
int bias, int bias,
int round) int round)
=> Av1IntraPredictorBase.Narrow( => Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector512.Create(bias), round), RoundPowerOfTwo(lower + Vector512.Create(bias), round),
RoundPowerOfTwo(upper + Vector512.Create(bias), round)); RoundPowerOfTwo(upper + Vector512.Create(bias), round));
@ -530,21 +530,21 @@ internal static partial class Av1CompoundInterPredictor
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<ushort> PrepareVertical(Vector128<int> lower, Vector128<int> upper) public static Vector128<ushort> PrepareVertical(Vector128<int> lower, Vector128<int> upper)
=> Av1IntraPredictorBase.Narrow( => Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector128.Create(VerticalBias), CompoundRound1Bits), RoundPowerOfTwo(lower + Vector128.Create(VerticalBias), CompoundRound1Bits),
RoundPowerOfTwo(upper + Vector128.Create(VerticalBias), CompoundRound1Bits)).AsUInt16(); RoundPowerOfTwo(upper + Vector128.Create(VerticalBias), CompoundRound1Bits)).AsUInt16();
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<ushort> PrepareVertical(Vector256<int> lower, Vector256<int> upper) public static Vector256<ushort> PrepareVertical(Vector256<int> lower, Vector256<int> upper)
=> Av1IntraPredictorBase.Narrow( => Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector256.Create(VerticalBias), CompoundRound1Bits), RoundPowerOfTwo(lower + Vector256.Create(VerticalBias), CompoundRound1Bits),
RoundPowerOfTwo(upper + Vector256.Create(VerticalBias), CompoundRound1Bits)).AsUInt16(); RoundPowerOfTwo(upper + Vector256.Create(VerticalBias), CompoundRound1Bits)).AsUInt16();
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<ushort> PrepareVertical(Vector512<int> lower, Vector512<int> upper) public static Vector512<ushort> PrepareVertical(Vector512<int> lower, Vector512<int> upper)
=> Av1IntraPredictorBase.Narrow( => Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector512.Create(VerticalBias), CompoundRound1Bits), RoundPowerOfTwo(lower + Vector512.Create(VerticalBias), CompoundRound1Bits),
RoundPowerOfTwo(upper + Vector512.Create(VerticalBias), CompoundRound1Bits)).AsUInt16(); RoundPowerOfTwo(upper + Vector512.Create(VerticalBias), CompoundRound1Bits)).AsUInt16();
@ -559,7 +559,7 @@ internal static partial class Av1CompoundInterPredictor
Vector128<int> lower, Vector128<int> lower,
Vector128<int> upper, Vector128<int> upper,
int bias) int bias)
=> Av1IntraPredictorBase.Narrow( => Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector128.Create(bias), CompoundRound1Bits), RoundPowerOfTwo(lower + Vector128.Create(bias), CompoundRound1Bits),
RoundPowerOfTwo(upper + Vector128.Create(bias), CompoundRound1Bits)).AsUInt16(); RoundPowerOfTwo(upper + Vector128.Create(bias), CompoundRound1Bits)).AsUInt16();
@ -569,7 +569,7 @@ internal static partial class Av1CompoundInterPredictor
Vector256<int> lower, Vector256<int> lower,
Vector256<int> upper, Vector256<int> upper,
int bias) int bias)
=> Av1IntraPredictorBase.Narrow( => Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector256.Create(bias), CompoundRound1Bits), RoundPowerOfTwo(lower + Vector256.Create(bias), CompoundRound1Bits),
RoundPowerOfTwo(upper + Vector256.Create(bias), CompoundRound1Bits)).AsUInt16(); RoundPowerOfTwo(upper + Vector256.Create(bias), CompoundRound1Bits)).AsUInt16();
@ -579,7 +579,7 @@ internal static partial class Av1CompoundInterPredictor
Vector512<int> lower, Vector512<int> lower,
Vector512<int> upper, Vector512<int> upper,
int bias) int bias)
=> Av1IntraPredictorBase.Narrow( => Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(lower + Vector512.Create(bias), CompoundRound1Bits), RoundPowerOfTwo(lower + Vector512.Create(bias), CompoundRound1Bits),
RoundPowerOfTwo(upper + Vector512.Create(bias), CompoundRound1Bits)).AsUInt16(); RoundPowerOfTwo(upper + Vector512.Create(bias), CompoundRound1Bits)).AsUInt16();
} }

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

@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.Operator.cs

@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

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

@ -6,7 +6,7 @@ using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDifferenceWeightedMaskBuilder.Operator.cs

@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

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

@ -7,7 +7,7 @@ using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.Operator.cs

@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

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

@ -6,7 +6,7 @@ using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateMaskBlendPredictor.Operator.cs

@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

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

@ -6,7 +6,7 @@ using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

50
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundMaskBlendPredictor.Operator.cs

@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
@ -111,10 +111,10 @@ internal static partial class Av1CompoundMaskBlendPredictor
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Blend(Vector128<byte> first, Vector128<byte> second, Vector128<byte> alpha) public static Vector128<byte> Blend(Vector128<byte> first, Vector128<byte> second, Vector128<byte> alpha)
{ {
Av1IntraPredictorBase.Widen(first, out Vector128<int> first0, out Vector128<int> first1, out Vector128<int> first2, out Vector128<int> first3); Av1NonDirectionalIntraPredictorBase.Widen(first, out Vector128<int> first0, out Vector128<int> first1, out Vector128<int> first2, out Vector128<int> first3);
Av1IntraPredictorBase.Widen(second, out Vector128<int> second0, out Vector128<int> second1, out Vector128<int> second2, out Vector128<int> second3); Av1NonDirectionalIntraPredictorBase.Widen(second, out Vector128<int> second0, out Vector128<int> second1, out Vector128<int> second2, out Vector128<int> second3);
Av1IntraPredictorBase.Widen(alpha, out Vector128<int> alpha0, out Vector128<int> alpha1, out Vector128<int> alpha2, out Vector128<int> alpha3); Av1NonDirectionalIntraPredictorBase.Widen(alpha, out Vector128<int> alpha0, out Vector128<int> alpha1, out Vector128<int> alpha2, out Vector128<int> alpha3);
return Av1IntraPredictorBase.Narrow( return Av1NonDirectionalIntraPredictorBase.Narrow(
Blend(first0, second0, alpha0), Blend(first0, second0, alpha0),
Blend(first1, second1, alpha1), Blend(first1, second1, alpha1),
Blend(first2, second2, alpha2), Blend(first2, second2, alpha2),
@ -125,10 +125,10 @@ internal static partial class Av1CompoundMaskBlendPredictor
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<byte> Blend(Vector256<byte> first, Vector256<byte> second, Vector256<byte> alpha) public static Vector256<byte> Blend(Vector256<byte> first, Vector256<byte> second, Vector256<byte> alpha)
{ {
Av1IntraPredictorBase.Widen(first, out Vector256<int> first0, out Vector256<int> first1, out Vector256<int> first2, out Vector256<int> first3); Av1NonDirectionalIntraPredictorBase.Widen(first, out Vector256<int> first0, out Vector256<int> first1, out Vector256<int> first2, out Vector256<int> first3);
Av1IntraPredictorBase.Widen(second, out Vector256<int> second0, out Vector256<int> second1, out Vector256<int> second2, out Vector256<int> second3); Av1NonDirectionalIntraPredictorBase.Widen(second, out Vector256<int> second0, out Vector256<int> second1, out Vector256<int> second2, out Vector256<int> second3);
Av1IntraPredictorBase.Widen(alpha, out Vector256<int> alpha0, out Vector256<int> alpha1, out Vector256<int> alpha2, out Vector256<int> alpha3); Av1NonDirectionalIntraPredictorBase.Widen(alpha, out Vector256<int> alpha0, out Vector256<int> alpha1, out Vector256<int> alpha2, out Vector256<int> alpha3);
return Av1IntraPredictorBase.Narrow( return Av1NonDirectionalIntraPredictorBase.Narrow(
Blend(first0, second0, alpha0), Blend(first0, second0, alpha0),
Blend(first1, second1, alpha1), Blend(first1, second1, alpha1),
Blend(first2, second2, alpha2), Blend(first2, second2, alpha2),
@ -139,10 +139,10 @@ internal static partial class Av1CompoundMaskBlendPredictor
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<byte> Blend(Vector512<byte> first, Vector512<byte> second, Vector512<byte> alpha) public static Vector512<byte> Blend(Vector512<byte> first, Vector512<byte> second, Vector512<byte> alpha)
{ {
Av1IntraPredictorBase.Widen(first, out Vector512<int> first0, out Vector512<int> first1, out Vector512<int> first2, out Vector512<int> first3); Av1NonDirectionalIntraPredictorBase.Widen(first, out Vector512<int> first0, out Vector512<int> first1, out Vector512<int> first2, out Vector512<int> first3);
Av1IntraPredictorBase.Widen(second, out Vector512<int> second0, out Vector512<int> second1, out Vector512<int> second2, out Vector512<int> second3); Av1NonDirectionalIntraPredictorBase.Widen(second, out Vector512<int> second0, out Vector512<int> second1, out Vector512<int> second2, out Vector512<int> second3);
Av1IntraPredictorBase.Widen(alpha, out Vector512<int> alpha0, out Vector512<int> alpha1, out Vector512<int> alpha2, out Vector512<int> alpha3); Av1NonDirectionalIntraPredictorBase.Widen(alpha, out Vector512<int> alpha0, out Vector512<int> alpha1, out Vector512<int> alpha2, out Vector512<int> alpha3);
return Av1IntraPredictorBase.Narrow( return Av1NonDirectionalIntraPredictorBase.Narrow(
Blend(first0, second0, alpha0), Blend(first0, second0, alpha0),
Blend(first1, second1, alpha1), Blend(first1, second1, alpha1),
Blend(first2, second2, alpha2), Blend(first2, second2, alpha2),
@ -153,30 +153,30 @@ internal static partial class Av1CompoundMaskBlendPredictor
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<ushort> Blend(Vector128<ushort> first, Vector128<ushort> second, Vector128<ushort> alpha) public static Vector128<ushort> Blend(Vector128<ushort> first, Vector128<ushort> second, Vector128<ushort> alpha)
{ {
Av1IntraPredictorBase.Widen(first.AsInt16(), out Vector128<int> first0, out Vector128<int> first1); Av1NonDirectionalIntraPredictorBase.Widen(first.AsInt16(), out Vector128<int> first0, out Vector128<int> first1);
Av1IntraPredictorBase.Widen(second.AsInt16(), out Vector128<int> second0, out Vector128<int> second1); Av1NonDirectionalIntraPredictorBase.Widen(second.AsInt16(), out Vector128<int> second0, out Vector128<int> second1);
Av1IntraPredictorBase.Widen(alpha.AsInt16(), out Vector128<int> alpha0, out Vector128<int> alpha1); Av1NonDirectionalIntraPredictorBase.Widen(alpha.AsInt16(), out Vector128<int> alpha0, out Vector128<int> alpha1);
return Av1IntraPredictorBase.Narrow(Blend(first0, second0, alpha0), Blend(first1, second1, alpha1)).AsUInt16(); return Av1NonDirectionalIntraPredictorBase.Narrow(Blend(first0, second0, alpha0), Blend(first1, second1, alpha1)).AsUInt16();
} }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<ushort> Blend(Vector256<ushort> first, Vector256<ushort> second, Vector256<ushort> alpha) public static Vector256<ushort> Blend(Vector256<ushort> first, Vector256<ushort> second, Vector256<ushort> alpha)
{ {
Av1IntraPredictorBase.Widen(first.AsInt16(), out Vector256<int> first0, out Vector256<int> first1); Av1NonDirectionalIntraPredictorBase.Widen(first.AsInt16(), out Vector256<int> first0, out Vector256<int> first1);
Av1IntraPredictorBase.Widen(second.AsInt16(), out Vector256<int> second0, out Vector256<int> second1); Av1NonDirectionalIntraPredictorBase.Widen(second.AsInt16(), out Vector256<int> second0, out Vector256<int> second1);
Av1IntraPredictorBase.Widen(alpha.AsInt16(), out Vector256<int> alpha0, out Vector256<int> alpha1); Av1NonDirectionalIntraPredictorBase.Widen(alpha.AsInt16(), out Vector256<int> alpha0, out Vector256<int> alpha1);
return Av1IntraPredictorBase.Narrow(Blend(first0, second0, alpha0), Blend(first1, second1, alpha1)).AsUInt16(); return Av1NonDirectionalIntraPredictorBase.Narrow(Blend(first0, second0, alpha0), Blend(first1, second1, alpha1)).AsUInt16();
} }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<ushort> Blend(Vector512<ushort> first, Vector512<ushort> second, Vector512<ushort> alpha) public static Vector512<ushort> Blend(Vector512<ushort> first, Vector512<ushort> second, Vector512<ushort> alpha)
{ {
Av1IntraPredictorBase.Widen(first.AsInt16(), out Vector512<int> first0, out Vector512<int> first1); Av1NonDirectionalIntraPredictorBase.Widen(first.AsInt16(), out Vector512<int> first0, out Vector512<int> first1);
Av1IntraPredictorBase.Widen(second.AsInt16(), out Vector512<int> second0, out Vector512<int> second1); Av1NonDirectionalIntraPredictorBase.Widen(second.AsInt16(), out Vector512<int> second0, out Vector512<int> second1);
Av1IntraPredictorBase.Widen(alpha.AsInt16(), out Vector512<int> alpha0, out Vector512<int> alpha1); Av1NonDirectionalIntraPredictorBase.Widen(alpha.AsInt16(), out Vector512<int> alpha0, out Vector512<int> alpha1);
return Av1IntraPredictorBase.Narrow(Blend(first0, second0, alpha0), Blend(first1, second1, alpha1)).AsUInt16(); return Av1NonDirectionalIntraPredictorBase.Narrow(Blend(first0, second0, alpha0), Blend(first1, second1, alpha1)).AsUInt16();
} }
/// <summary> /// <summary>

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

@ -6,7 +6,7 @@ using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1DifferenceWeightedMaskBuilder.Operator.cs

@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

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

@ -6,7 +6,7 @@ using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

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

@ -6,7 +6,7 @@ using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

14
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.Operator.cs

@ -4,7 +4,7 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
@ -272,7 +272,7 @@ internal static partial class Av1ScaledInterPredictor
Vector128<int> initial, Vector128<int> initial,
out Vector128<int> result0, out Vector128<int> result0,
out Vector128<int> result1) out Vector128<int> result1)
=> Av1InterPredictor.Convolve( => Av1TranslationalInterPredictor.Convolve(
ref source, ref source,
sourceStride, sourceStride,
sourceOffset, sourceOffset,
@ -293,7 +293,7 @@ internal static partial class Av1ScaledInterPredictor
Vector256<int> initial, Vector256<int> initial,
out Vector256<int> result0, out Vector256<int> result0,
out Vector256<int> result1) out Vector256<int> result1)
=> Av1InterPredictor.Convolve( => Av1TranslationalInterPredictor.Convolve(
ref source, ref source,
sourceStride, sourceStride,
sourceOffset, sourceOffset,
@ -314,7 +314,7 @@ internal static partial class Av1ScaledInterPredictor
Vector512<int> initial, Vector512<int> initial,
out Vector512<int> result0, out Vector512<int> result0,
out Vector512<int> result1) out Vector512<int> result1)
=> Av1InterPredictor.Convolve( => Av1TranslationalInterPredictor.Convolve(
ref source, ref source,
sourceStride, sourceStride,
sourceOffset, sourceOffset,
@ -426,7 +426,7 @@ internal static partial class Av1ScaledInterPredictor
Vector128<int> result1, Vector128<int> result1,
int bitDepth) int bitDepth)
where T : unmanaged where T : unmanaged
=> Av1IntraPredictorBase.Narrow(result0, result1) => Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1)
.AsUInt16() .AsUInt16()
.StoreUnsafe(ref Unsafe.As<T, ushort>(ref destination), (nuint)index); .StoreUnsafe(ref Unsafe.As<T, ushort>(ref destination), (nuint)index);
@ -439,7 +439,7 @@ internal static partial class Av1ScaledInterPredictor
Vector256<int> result1, Vector256<int> result1,
int bitDepth) int bitDepth)
where T : unmanaged where T : unmanaged
=> Av1IntraPredictorBase.Narrow(result0, result1) => Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1)
.AsUInt16() .AsUInt16()
.StoreUnsafe(ref Unsafe.As<T, ushort>(ref destination), (nuint)index); .StoreUnsafe(ref Unsafe.As<T, ushort>(ref destination), (nuint)index);
@ -452,7 +452,7 @@ internal static partial class Av1ScaledInterPredictor
Vector512<int> result1, Vector512<int> result1,
int bitDepth) int bitDepth)
where T : unmanaged where T : unmanaged
=> Av1IntraPredictorBase.Narrow(result0, result1) => Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1)
.AsUInt16() .AsUInt16()
.StoreUnsafe(ref Unsafe.As<T, ushort>(ref destination), (nuint)index); .StoreUnsafe(ref Unsafe.As<T, ushort>(ref destination), (nuint)index);
} }

8
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.cs

@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
@ -423,7 +423,7 @@ internal static partial class Av1ScaledInterPredictor
horizontalBias, horizontalBias,
round0); round0);
Av1IntraPredictorBase.Narrow(result, Vector512<int>.Zero) Av1NonDirectionalIntraPredictorBase.Narrow(result, Vector512<int>.Zero)
.GetLower() .GetLower()
.StoreUnsafe(ref scratchRow, (nuint)column); .StoreUnsafe(ref scratchRow, (nuint)column);
} }
@ -443,7 +443,7 @@ internal static partial class Av1ScaledInterPredictor
horizontalBias, horizontalBias,
round0); round0);
Av1IntraPredictorBase.Narrow(result, Vector256<int>.Zero) Av1NonDirectionalIntraPredictorBase.Narrow(result, Vector256<int>.Zero)
.GetLower() .GetLower()
.StoreUnsafe(ref scratchRow, (nuint)column); .StoreUnsafe(ref scratchRow, (nuint)column);
} }
@ -462,7 +462,7 @@ internal static partial class Av1ScaledInterPredictor
horizontalBias, horizontalBias,
round0); round0);
Av1IntraPredictorBase.Narrow(result, Vector128<int>.Zero) Av1NonDirectionalIntraPredictorBase.Narrow(result, Vector128<int>.Zero)
.GetLower() .GetLower()
.StoreUnsafe(ref scratchRow, (nuint)column); .StoreUnsafe(ref scratchRow, (nuint)column);
} }

26
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Arithmetic.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Arithmetic.cs

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
/// <content> /// <content>
/// Provides lane-wise convolution, rounding, clipping, and packing shared by every interpolation filter. /// Provides lane-wise convolution, rounding, clipping, and packing shared by every interpolation filter.
/// </content> /// </content>
internal static partial class Av1InterPredictor internal static partial class Av1TranslationalInterPredictor
{ {
/// <summary> /// <summary>
/// Convolves sixteen adjacent 8-bit samples into four signed 32-bit accumulator vectors. /// Convolves sixteen adjacent 8-bit samples into four signed 32-bit accumulator vectors.
@ -35,7 +35,7 @@ internal static partial class Av1InterPredictor
for (int tap = 0; tap < tapCount; tap++) for (int tap = 0; tap < tapCount; tap++)
{ {
Vector128<byte> samples = Vector128.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column); Vector128<byte> samples = Vector128.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column);
Av1IntraPredictorBase.Widen(samples, out Vector128<int> samples0, out Vector128<int> samples1, out Vector128<int> samples2, out Vector128<int> samples3); Av1NonDirectionalIntraPredictorBase.Widen(samples, out Vector128<int> samples0, out Vector128<int> samples1, out Vector128<int> samples2, out Vector128<int> samples3);
Vector128<int> coefficient = Vector128.Create((int)Unsafe.Add(ref coefficients, tap)); Vector128<int> coefficient = Vector128.Create((int)Unsafe.Add(ref coefficients, tap));
// Each widened vector retains four consecutive source columns. Applying the same tap coefficient to all // Each widened vector retains four consecutive source columns. Applying the same tap coefficient to all
@ -71,7 +71,7 @@ internal static partial class Av1InterPredictor
for (int tap = 0; tap < tapCount; tap++) for (int tap = 0; tap < tapCount; tap++)
{ {
Vector256<byte> samples = Vector256.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column); Vector256<byte> samples = Vector256.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column);
Av1IntraPredictorBase.Widen(samples, out Vector256<int> samples0, out Vector256<int> samples1, out Vector256<int> samples2, out Vector256<int> samples3); Av1NonDirectionalIntraPredictorBase.Widen(samples, out Vector256<int> samples0, out Vector256<int> samples1, out Vector256<int> samples2, out Vector256<int> samples3);
Vector256<int> coefficient = Vector256.Create((int)Unsafe.Add(ref coefficients, tap)); Vector256<int> coefficient = Vector256.Create((int)Unsafe.Add(ref coefficients, tap));
result0 += samples0 * coefficient; result0 += samples0 * coefficient;
@ -105,7 +105,7 @@ internal static partial class Av1InterPredictor
for (int tap = 0; tap < tapCount; tap++) for (int tap = 0; tap < tapCount; tap++)
{ {
Vector512<byte> samples = Vector512.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column); Vector512<byte> samples = Vector512.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column);
Av1IntraPredictorBase.Widen(samples, out Vector512<int> samples0, out Vector512<int> samples1, out Vector512<int> samples2, out Vector512<int> samples3); Av1NonDirectionalIntraPredictorBase.Widen(samples, out Vector512<int> samples0, out Vector512<int> samples1, out Vector512<int> samples2, out Vector512<int> samples3);
Vector512<int> coefficient = Vector512.Create((int)Unsafe.Add(ref coefficients, tap)); Vector512<int> coefficient = Vector512.Create((int)Unsafe.Add(ref coefficients, tap));
result0 += samples0 * coefficient; result0 += samples0 * coefficient;
@ -135,7 +135,7 @@ internal static partial class Av1InterPredictor
for (int tap = 0; tap < tapCount; tap++) for (int tap = 0; tap < tapCount; tap++)
{ {
Vector128<short> samples = Vector128.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column); Vector128<short> samples = Vector128.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column);
Av1IntraPredictorBase.Widen(samples, out Vector128<int> samples0, out Vector128<int> samples1); Av1NonDirectionalIntraPredictorBase.Widen(samples, out Vector128<int> samples0, out Vector128<int> samples1);
Vector128<int> coefficient = Vector128.Create((int)Unsafe.Add(ref coefficients, tap)); Vector128<int> coefficient = Vector128.Create((int)Unsafe.Add(ref coefficients, tap));
// Reconstructed 10- and 12-bit samples and biased 2D intermediates are below short.MaxValue, so signed // Reconstructed 10- and 12-bit samples and biased 2D intermediates are below short.MaxValue, so signed
@ -165,7 +165,7 @@ internal static partial class Av1InterPredictor
for (int tap = 0; tap < tapCount; tap++) for (int tap = 0; tap < tapCount; tap++)
{ {
Vector256<short> samples = Vector256.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column); Vector256<short> samples = Vector256.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column);
Av1IntraPredictorBase.Widen(samples, out Vector256<int> samples0, out Vector256<int> samples1); Av1NonDirectionalIntraPredictorBase.Widen(samples, out Vector256<int> samples0, out Vector256<int> samples1);
Vector256<int> coefficient = Vector256.Create((int)Unsafe.Add(ref coefficients, tap)); Vector256<int> coefficient = Vector256.Create((int)Unsafe.Add(ref coefficients, tap));
result0 += samples0 * coefficient; result0 += samples0 * coefficient;
result1 += samples1 * coefficient; result1 += samples1 * coefficient;
@ -192,7 +192,7 @@ internal static partial class Av1InterPredictor
for (int tap = 0; tap < tapCount; tap++) for (int tap = 0; tap < tapCount; tap++)
{ {
Vector512<short> samples = Vector512.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column); Vector512<short> samples = Vector512.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column);
Av1IntraPredictorBase.Widen(samples, out Vector512<int> samples0, out Vector512<int> samples1); Av1NonDirectionalIntraPredictorBase.Widen(samples, out Vector512<int> samples0, out Vector512<int> samples1);
Vector512<int> coefficient = Vector512.Create((int)Unsafe.Add(ref coefficients, tap)); Vector512<int> coefficient = Vector512.Create((int)Unsafe.Add(ref coefficients, tap));
result0 += samples0 * coefficient; result0 += samples0 * coefficient;
result1 += samples1 * coefficient; result1 += samples1 * coefficient;
@ -231,7 +231,7 @@ internal static partial class Av1InterPredictor
result1 = Vector128.Clamp(result1, Vector128<int>.Zero, maximum); result1 = Vector128.Clamp(result1, Vector128<int>.Zero, maximum);
result2 = Vector128.Clamp(result2, Vector128<int>.Zero, maximum); result2 = Vector128.Clamp(result2, Vector128<int>.Zero, maximum);
result3 = Vector128.Clamp(result3, Vector128<int>.Zero, maximum); result3 = Vector128.Clamp(result3, Vector128<int>.Zero, maximum);
return Av1IntraPredictorBase.Narrow(result0, result1, result2, result3); return Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1, result2, result3);
} }
/// <summary> /// <summary>
@ -245,7 +245,7 @@ internal static partial class Av1InterPredictor
result1 = Vector256.Clamp(result1, Vector256<int>.Zero, maximum); result1 = Vector256.Clamp(result1, Vector256<int>.Zero, maximum);
result2 = Vector256.Clamp(result2, Vector256<int>.Zero, maximum); result2 = Vector256.Clamp(result2, Vector256<int>.Zero, maximum);
result3 = Vector256.Clamp(result3, Vector256<int>.Zero, maximum); result3 = Vector256.Clamp(result3, Vector256<int>.Zero, maximum);
return Av1IntraPredictorBase.Narrow(result0, result1, result2, result3); return Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1, result2, result3);
} }
/// <summary> /// <summary>
@ -259,7 +259,7 @@ internal static partial class Av1InterPredictor
result1 = Vector512.Clamp(result1, Vector512<int>.Zero, maximum); result1 = Vector512.Clamp(result1, Vector512<int>.Zero, maximum);
result2 = Vector512.Clamp(result2, Vector512<int>.Zero, maximum); result2 = Vector512.Clamp(result2, Vector512<int>.Zero, maximum);
result3 = Vector512.Clamp(result3, Vector512<int>.Zero, maximum); result3 = Vector512.Clamp(result3, Vector512<int>.Zero, maximum);
return Av1IntraPredictorBase.Narrow(result0, result1, result2, result3); return Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1, result2, result3);
} }
/// <summary> /// <summary>
@ -271,7 +271,7 @@ internal static partial class Av1InterPredictor
Vector128<int> maximum = Vector128.Create(maximumValue); Vector128<int> maximum = Vector128.Create(maximumValue);
result0 = Vector128.Clamp(result0, Vector128<int>.Zero, maximum); result0 = Vector128.Clamp(result0, Vector128<int>.Zero, maximum);
result1 = Vector128.Clamp(result1, Vector128<int>.Zero, maximum); result1 = Vector128.Clamp(result1, Vector128<int>.Zero, maximum);
return Av1IntraPredictorBase.Narrow(result0, result1).AsUInt16(); return Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1).AsUInt16();
} }
/// <summary> /// <summary>
@ -283,7 +283,7 @@ internal static partial class Av1InterPredictor
Vector256<int> maximum = Vector256.Create(maximumValue); Vector256<int> maximum = Vector256.Create(maximumValue);
result0 = Vector256.Clamp(result0, Vector256<int>.Zero, maximum); result0 = Vector256.Clamp(result0, Vector256<int>.Zero, maximum);
result1 = Vector256.Clamp(result1, Vector256<int>.Zero, maximum); result1 = Vector256.Clamp(result1, Vector256<int>.Zero, maximum);
return Av1IntraPredictorBase.Narrow(result0, result1).AsUInt16(); return Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1).AsUInt16();
} }
/// <summary> /// <summary>
@ -295,7 +295,7 @@ internal static partial class Av1InterPredictor
Vector512<int> maximum = Vector512.Create(maximumValue); Vector512<int> maximum = Vector512.Create(maximumValue);
result0 = Vector512.Clamp(result0, Vector512<int>.Zero, maximum); result0 = Vector512.Clamp(result0, Vector512<int>.Zero, maximum);
result1 = Vector512.Clamp(result1, Vector512<int>.Zero, maximum); result1 = Vector512.Clamp(result1, Vector512<int>.Zero, maximum);
return Av1IntraPredictorBase.Narrow(result0, result1).AsUInt16(); return Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1).AsUInt16();
} }
/// <summary> /// <summary>

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.BilinearOperator.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.BilinearOperator.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
/// <content> /// <content>
/// Defines bilinear interpolation for translational inter prediction. /// Defines bilinear interpolation for translational inter prediction.
/// </content> /// </content>
internal static partial class Av1InterPredictor internal static partial class Av1TranslationalInterPredictor
{ {
/// <summary> /// <summary>
/// Selects bilinear interpolation coefficients. /// Selects bilinear interpolation coefficients.

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Dispatch.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Dispatch.cs

@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
/// <content> /// <content>
/// Selects interpolation filters and the widest supported traversal for a translational prediction block. /// Selects interpolation filters and the widest supported traversal for a translational prediction block.
/// </content> /// </content>
internal static partial class Av1InterPredictor internal static partial class Av1TranslationalInterPredictor
{ {
/// <summary> /// <summary>
/// Selects an 8-bit horizontal interpolation operator. /// Selects an 8-bit horizontal interpolation operator.

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Filters.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Filters.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
/// <content> /// <content>
/// Provides the normative Q7 interpolation coefficients used by AV1 inter prediction. /// Provides the normative Q7 interpolation coefficients used by AV1 inter prediction.
/// </content> /// </content>
internal static partial class Av1InterPredictor internal static partial class Av1TranslationalInterPredictor
{ {
/// <summary> /// <summary>
/// The number of stored coefficient positions in every decoder interpolation kernel. /// The number of stored coefficient positions in every decoder interpolation kernel.

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.OneDimension.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.OneDimension.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
/// <content> /// <content>
/// Provides SIMD kernels for horizontal-only and vertical-only single-reference filtering. /// Provides SIMD kernels for horizontal-only and vertical-only single-reference filtering.
/// </content> /// </content>
internal static partial class Av1InterPredictor internal static partial class Av1TranslationalInterPredictor
{ {
/// <summary> /// <summary>
/// Filters an 8-bit block in sixteen-sample vectors. /// Filters an 8-bit block in sixteen-sample vectors.

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Operator.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Operator.cs

@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
/// <content> /// <content>
/// Defines interpolation operators and the generic traversal used by translational inter prediction. /// Defines interpolation operators and the generic traversal used by translational inter prediction.
/// </content> /// </content>
internal static partial class Av1InterPredictor internal static partial class Av1TranslationalInterPredictor
{ {
/// <summary> /// <summary>
/// Supplies the normative Q7 coefficient kernel for one AV1 interpolation-filter family. /// Supplies the normative Q7 coefficient kernel for one AV1 interpolation-filter family.

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.RegularOperator.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.RegularOperator.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
/// <content> /// <content>
/// Defines regular interpolation for translational inter prediction. /// Defines regular interpolation for translational inter prediction.
/// </content> /// </content>
internal static partial class Av1InterPredictor internal static partial class Av1TranslationalInterPredictor
{ {
/// <summary> /// <summary>
/// Selects regular interpolation coefficients. /// Selects regular interpolation coefficients.

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.SharpOperator.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.SharpOperator.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
/// <content> /// <content>
/// Defines sharp interpolation for translational inter prediction. /// Defines sharp interpolation for translational inter prediction.
/// </content> /// </content>
internal static partial class Av1InterPredictor internal static partial class Av1TranslationalInterPredictor
{ {
/// <summary> /// <summary>
/// Selects sharp interpolation coefficients. /// Selects sharp interpolation coefficients.

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.SmoothOperator.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.SmoothOperator.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
/// <content> /// <content>
/// Defines smooth interpolation for translational inter prediction. /// Defines smooth interpolation for translational inter prediction.
/// </content> /// </content>
internal static partial class Av1InterPredictor internal static partial class Av1TranslationalInterPredictor
{ {
/// <summary> /// <summary>
/// Selects smooth interpolation coefficients. /// Selects smooth interpolation coefficients.

18
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.TwoDimensions.Byte.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.TwoDimensions.Byte.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
/// <content> /// <content>
/// Provides separable SIMD convolution for 8-bit single-reference prediction. /// Provides separable SIMD convolution for 8-bit single-reference prediction.
/// </content> /// </content>
internal static partial class Av1InterPredictor internal static partial class Av1TranslationalInterPredictor
{ {
/// <summary> /// <summary>
/// Filters an 8-bit block in sixteen-sample vectors through caller-owned signed scratch. /// Filters an 8-bit block in sixteen-sample vectors through caller-owned signed scratch.
@ -63,8 +63,8 @@ internal static partial class Av1InterPredictor
out Vector128<int> result2, out Vector128<int> result2,
out Vector128<int> result3); out Vector128<int> result3);
Av1IntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow); Av1NonDirectionalIntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow);
Av1IntraPredictorBase.Narrow(RoundPowerOfTwo(result2, round0), RoundPowerOfTwo(result3, round0)).StoreUnsafe(ref scratchRow, (nuint)Vector128<short>.Count); Av1NonDirectionalIntraPredictorBase.Narrow(RoundPowerOfTwo(result2, round0), RoundPowerOfTwo(result3, round0)).StoreUnsafe(ref scratchRow, (nuint)Vector128<short>.Count);
continue; continue;
} }
@ -83,8 +83,8 @@ internal static partial class Av1InterPredictor
out Vector128<int> result2, out Vector128<int> result2,
out Vector128<int> result3); out Vector128<int> result3);
Av1IntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); Av1NonDirectionalIntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns);
Av1IntraPredictorBase.Narrow( Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(result2, round0), RoundPowerOfTwo(result2, round0),
RoundPowerOfTwo(result3, round0)).StoreUnsafe( RoundPowerOfTwo(result3, round0)).StoreUnsafe(
ref scratchRow, ref scratchRow,
@ -221,8 +221,8 @@ internal static partial class Av1InterPredictor
out Vector256<int> result2, out Vector256<int> result2,
out Vector256<int> result3); out Vector256<int> result3);
Av1IntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); Av1NonDirectionalIntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns);
Av1IntraPredictorBase.Narrow( Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(result2, round0), RoundPowerOfTwo(result2, round0),
RoundPowerOfTwo(result3, round0)).StoreUnsafe( RoundPowerOfTwo(result3, round0)).StoreUnsafe(
ref scratchRow, ref scratchRow,
@ -328,8 +328,8 @@ internal static partial class Av1InterPredictor
out Vector512<int> result2, out Vector512<int> result2,
out Vector512<int> result3); out Vector512<int> result3);
Av1IntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); Av1NonDirectionalIntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns);
Av1IntraPredictorBase.Narrow( Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(result2, round0), RoundPowerOfTwo(result2, round0),
RoundPowerOfTwo(result3, round0)).StoreUnsafe( RoundPowerOfTwo(result3, round0)).StoreUnsafe(
ref scratchRow, ref scratchRow,

10
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.TwoDimensions.UInt16.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.TwoDimensions.UInt16.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
/// <content> /// <content>
/// Provides separable SIMD convolution for unsigned 16-bit single-reference prediction. /// Provides separable SIMD convolution for unsigned 16-bit single-reference prediction.
/// </content> /// </content>
internal static partial class Av1InterPredictor internal static partial class Av1TranslationalInterPredictor
{ {
/// <summary> /// <summary>
/// Filters a high-bit-depth block in eight-sample vectors through caller-owned signed scratch. /// Filters a high-bit-depth block in eight-sample vectors through caller-owned signed scratch.
@ -67,7 +67,7 @@ internal static partial class Av1InterPredictor
out Vector128<int> result0, out Vector128<int> result0,
out Vector128<int> result1); out Vector128<int> result1);
Av1IntraPredictorBase.Narrow( Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result0, round0),
RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow); RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow);
@ -87,7 +87,7 @@ internal static partial class Av1InterPredictor
out Vector128<int> result0, out Vector128<int> result0,
out Vector128<int> result1); out Vector128<int> result1);
Av1IntraPredictorBase.Narrow( Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result0, round0),
RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns);
} }
@ -223,7 +223,7 @@ internal static partial class Av1InterPredictor
out Vector256<int> result0, out Vector256<int> result0,
out Vector256<int> result1); out Vector256<int> result1);
Av1IntraPredictorBase.Narrow( Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result0, round0),
RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns);
} }
@ -337,7 +337,7 @@ internal static partial class Av1InterPredictor
out Vector512<int> result0, out Vector512<int> result0,
out Vector512<int> result1); out Vector512<int> result1);
Av1IntraPredictorBase.Narrow( Av1NonDirectionalIntraPredictorBase.Narrow(
RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result0, round0),
RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns);
} }

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.cs → src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.cs

@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
/// copy or one-dimensional filtering. /// copy or one-dimensional filtering.
/// </para> /// </para>
/// </remarks> /// </remarks>
internal static partial class Av1InterPredictor internal static partial class Av1TranslationalInterPredictor
{ {
/// <summary> /// <summary>
/// The number of fractional bits in each interpolation coefficient. /// The number of fractional bits in each interpolation coefficient.

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.Filters.cs

@ -1,7 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

2
src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.Operator.cs

@ -4,7 +4,7 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

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

@ -6,7 +6,7 @@ using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1InterPredictor; using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;

32
src/ImageSharp/Formats/Heif/Av1/Readme.md

@ -1,27 +1,26 @@
# Open Bitstream Unit # Open Bitstream Unit
An OBU unit is a unit of parameters encoded in a bitstream format. In AVIF, it contains a single frame. An OBU is a unit of syntax encoded in an AV1 bitstream. HEIF image items can contain still-picture,
This frame is coded using no other frame as reference, it is a so called INTRA frame. AV1 movie encoding also defines INTER frames, progressive, or dependent-frame AV1 payloads, so the decoder handles both intra and inter frames.
which are predictions of one or more other frames. INTER frames are not used in AVIF and therefore this coded ignores INTER frames.
An OBU section for AVIF consists of the following headers: An OBU section for AVIF consists of the following headers:
## Temporal delimiter ## Temporal delimiter
In AV1 movies this is a time point. Although irrelevant for AVIF, most implementtions write one such delimiter at the start of the section. In AV1 sequences this marks a temporal-unit boundary. Many encoders write one at the start of the payload.
## Sequence header ## Sequence header
Common herader for a list (or sequence) of frames. For AVIF, this is exaclty 1 frame. For AVIF, this header can be reduced in size when its `ReducedStillPictureHerader` parameter is true. This is the common header for a sequence of frames. A still picture can use the reduced syntax selected by
This setting is recommended, as all the extra parameters are not applicable for AVIF. `ReducedStillPictureHeader`; progressive and dependent-frame payloads use the complete sequence syntax.
## Frame header ## Frame header
Can be 3 different OBU types, which define a single INTRA frame in AVIF files. Frame-header, redundant-frame-header, and combined-frame OBUs define the syntax of a coded frame.
## Tile group ## Tile group
Defines the tiling parameters and contains the parameters its tile using a different coding. Defines the tile range and contains the entropy-coded payload for each tile in that range.
# Tiling # Tiling
@ -32,20 +31,18 @@ These parameters are contained in an OBU tile group header.
A tile consists of one or more superblocks. Superblocks can be either 64x64 or 128x128 pixels in size. A tile consists of one or more superblocks. Superblocks can be either 64x64 or 128x128 pixels in size.
This choice is made per frame, and is specified in the `ObuFrameHeader`. This choice is made per frame, and is specified in the `ObuFrameHeader`.
A superblock contains one or more partitions, to further devide the area. A superblock contains one or more partitions that subdivide the area.
## Partition ## Partition
A superblock contains one or more Partitions. The partition Type determines the number of partitions it is further split in. A superblock contains one or more partitions. The partition type determines how the area is split.
Paritions can contain other partitions and blocks. Partitions can contain other partitions and blocks.
## Block ## Block
## Transform Block ## Transform Block
A Transform Block is the smallest area of the image, which has the same transformation parameters. A block contains ore or more ModeInfos. A transform block is the smallest image area that shares transform parameters. A block contains one or more mode-information units.
## ModeInfo ## ModeInfo
@ -59,9 +56,11 @@ The smallest unit in the frame. It determines the parameters for an area of 4 by
[AVIF specification](https://aomediacodec.github.io/av1-avif) [AVIF specification](https://aomediacodec.github.io/av1-avif)
[AV1/AVIF reference implementation](http://gitlab.com/AOMediaCodec/SVT-AV1) [Official AV1 reference implementation](https://aomedia.googlesource.com/aom/)
[AOM's original development implementation](https://github.com/AOMediaCodec/libavif) [SVT-AV1 encoder](https://gitlab.com/AOMediaCodec/SVT-AV1)
[libavif AVIF container implementation](https://github.com/AOMediaCodec/libavif)
[Paper describing the techniques used in AV1](https://arxiv.org/pdf/2008.06091) [Paper describing the techniques used in AV1](https://arxiv.org/pdf/2008.06091)
@ -70,4 +69,3 @@ The smallest unit in the frame. It determines the parameters for an area of 4 by
[Netflix image repository](http://download.opencontent.netflix.com/?prefix=AV1/) [Netflix image repository](http://download.opencontent.netflix.com/?prefix=AV1/)
[AVIF sample images](https://github.com/link-u/avif-sample-images) [AVIF sample images](https://github.com/link-u/avif-sample-images)

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

@ -22,23 +22,28 @@ internal sealed class Av1ReferenceFrame : IDisposable
private Av1FrameBuffer<byte>? frameBuffer; private Av1FrameBuffer<byte>? frameBuffer;
/// <summary> /// <summary>
/// The independently retained entropy snapshot while this frame owner remains alive. /// The compact reference state and optional entropy snapshot retained while this frame occupies the reference map.
/// </summary> /// </summary>
private Av1FrameEntropyContext? entropyContext; private ReferenceOwnership? referenceOwnership;
/// <summary> /// <summary>
/// The decoder-session owner that receives <see cref="entropyContext"/> when this frame is released. /// Initializes a new instance of the <see cref="Av1ReferenceFrame"/> class for presentation-only ownership.
/// </summary> /// </summary>
private Av1FrameEntropyContexts? entropyContextOwner; /// <param name="frameBuffer">
/// The completed sample buffer. Ownership transfers to this instance when construction succeeds.
/// <summary> /// </param>
/// The shared decoded per-block state while this frame owns one lifetime lease. /// <param name="frameHeader">
/// </summary> /// The completed frame header associated with the reconstructed samples. The caller must not mutate the header
private Av1FrameInfo? frameInfo; /// after transferring it to this instance.
/// </param>
public Av1ReferenceFrame(Av1FrameBuffer<byte> frameBuffer, ObuFrameHeader frameHeader)
{
this.frameBuffer = frameBuffer;
this.FrameHeader = frameHeader;
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1ReferenceFrame"/> class and takes ownership of the decoded /// Initializes a new instance of the <see cref="Av1ReferenceFrame"/> class with retained compact reference state.
/// sample buffer.
/// </summary> /// </summary>
/// <param name="frameBuffer"> /// <param name="frameBuffer">
/// The completed sample buffer. Ownership transfers to this instance when construction succeeds. /// The completed sample buffer. Ownership transfers to this instance when construction succeeds.
@ -47,16 +52,11 @@ internal sealed class Av1ReferenceFrame : IDisposable
/// The completed frame header associated with the reconstructed samples. The caller must not mutate the header /// The completed frame header associated with the reconstructed samples. The caller must not mutate the header
/// after transferring it to this instance. /// after transferring it to this instance.
/// </param> /// </param>
/// <param name="frameInfo"> /// <param name="frameInfo">The completed reconstruction state from which reference syntax is retained.</param>
/// The completed per-block state associated with the reconstructed samples. The caller must not mutate the state
/// after transferring it to this instance.
/// </param>
public Av1ReferenceFrame(Av1FrameBuffer<byte> frameBuffer, ObuFrameHeader frameHeader, Av1FrameInfo frameInfo) public Av1ReferenceFrame(Av1FrameBuffer<byte> frameBuffer, ObuFrameHeader frameHeader, Av1FrameInfo frameInfo)
: this(frameBuffer, frameHeader)
{ {
this.frameBuffer = frameBuffer; this.referenceOwnership = new(frameInfo.AcquireReferenceState(), null);
this.FrameHeader = frameHeader;
this.frameInfo = frameInfo;
frameInfo.AddOwner();
} }
/// <summary> /// <summary>
@ -82,16 +82,24 @@ internal sealed class Av1ReferenceFrame : IDisposable
Av1FrameInfo frameInfo, Av1FrameInfo frameInfo,
Av1FrameEntropyContext entropyContext, Av1FrameEntropyContext entropyContext,
Av1FrameEntropyContexts entropyContextOwner) Av1FrameEntropyContexts entropyContextOwner)
: this(frameBuffer, frameHeader, frameInfo) : this(frameBuffer, frameHeader)
{ {
this.entropyContext = entropyContext; this.referenceOwnership = new(
this.entropyContextOwner = entropyContextOwner; frameInfo.AcquireReferenceState(),
new EntropyOwnership(entropyContext, entropyContextOwner));
} }
/// <summary> /// <summary>
/// Gets the completed sample buffer owned by this frame. /// Gets the completed sample buffer owned by this frame.
/// </summary> /// </summary>
public Av1FrameBuffer<byte> FrameBuffer => this.frameBuffer!; public Av1FrameBuffer<byte> FrameBuffer
{
get
{
return this.frameBuffer
?? throw new ObjectDisposedException(nameof(Av1ReferenceFrame));
}
}
/// <summary> /// <summary>
/// Gets the completed header that describes the retained frame. /// Gets the completed header that describes the retained frame.
@ -99,21 +107,41 @@ internal sealed class Av1ReferenceFrame : IDisposable
public ObuFrameHeader FrameHeader { get; } public ObuFrameHeader FrameHeader { get; }
/// <summary> /// <summary>
/// Gets the decoded per-block mode, motion, transform, and filter state associated with the retained frame. /// Gets the compact segment and motion state associated with the retained frame.
/// </summary> /// </summary>
public Av1FrameInfo FrameInfo => this.frameInfo!; public Av1FrameInfo.ReferenceState ReferenceState
{
get
{
ReferenceOwnership? ownership = this.referenceOwnership;
if (ownership is null)
{
throw new InvalidOperationException("A presentation-only AV1 frame has no retained reference state.");
}
return ownership.Value.ReferenceState;
}
}
/// <summary> /// <summary>
/// Gets the entropy context retained for primary-reference use, or <see langword="null"/> for a presentation-only /// Gets the entropy context retained for primary-reference use, or <see langword="null"/> for a presentation-only
/// frame. /// frame.
/// </summary> /// </summary>
public Av1FrameEntropyContext? EntropyContext => this.entropyContext; public Av1FrameEntropyContext? EntropyContext => this.referenceOwnership?.Entropy?.Context;
/// <summary> /// <summary>
/// Restores the retained frame context to the normative defaults selected by this frame's quantizer band. /// Restores the retained frame context to the normative defaults selected by this frame's quantizer band.
/// </summary> /// </summary>
public void ResetEntropyContext() public void ResetEntropyContext()
=> this.entropyContext!.ResetToDefaults(this.FrameHeader.QuantizationParameters.BaseQIndex); {
EntropyOwnership? entropy = this.referenceOwnership?.Entropy;
if (entropy is null)
{
throw new InvalidOperationException("The AV1 reference frame has no retained entropy context.");
}
entropy.Value.Context.ResetToDefaults(this.FrameHeader.QuantizationParameters.BaseQIndex);
}
/// <summary> /// <summary>
/// Transfers the completed sample planes out of this frame owner. /// Transfers the completed sample planes out of this frame owner.
@ -121,7 +149,9 @@ internal sealed class Av1ReferenceFrame : IDisposable
/// <returns>The completed sample planes now owned by the caller.</returns> /// <returns>The completed sample planes now owned by the caller.</returns>
public Av1FrameBuffer<byte> TakeFrameBuffer() public Av1FrameBuffer<byte> TakeFrameBuffer()
{ {
Av1FrameBuffer<byte> result = this.frameBuffer!; Av1FrameBuffer<byte> result = this.frameBuffer
?? throw new ObjectDisposedException(nameof(Av1ReferenceFrame));
this.frameBuffer = null; this.frameBuffer = null;
return result; return result;
} }
@ -131,19 +161,60 @@ internal sealed class Av1ReferenceFrame : IDisposable
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
Av1FrameEntropyContext? context = this.entropyContext; ReferenceOwnership? ownership = this.referenceOwnership;
this.entropyContext = null; this.referenceOwnership = null;
if (context is not null) if (ownership is not null)
{ {
// Nulling the field before returning the graph makes repeated disposal harmless and guarantees that one // Clearing the complete ownership state before returning either resource makes repeated disposal harmless
// shared frame owner occupying multiple reference slots returns its snapshot exactly once. // when one frame owner occupies multiple reference-map slots.
this.entropyContextOwner!.ReturnSnapshot(context); ReferenceOwnership activeOwnership = ownership.Value;
this.entropyContextOwner = null; EntropyOwnership? entropy = activeOwnership.Entropy;
if (entropy is not null)
{
EntropyOwnership activeEntropy = entropy.Value;
activeEntropy.Owner.ReturnSnapshot(activeEntropy.Context);
}
activeOwnership.ReferenceState.ReleaseOwner();
} }
this.frameBuffer?.Dispose(); this.frameBuffer?.Dispose();
this.frameBuffer = null; this.frameBuffer = null;
this.frameInfo?.ReleaseOwner(); }
this.frameInfo = null;
/// <summary>
/// Carries the complete state retained only by frames that can be selected as references.
/// </summary>
private readonly struct ReferenceOwnership(
Av1FrameInfo.ReferenceState referenceState,
EntropyOwnership? entropy)
{
/// <summary>
/// Gets the retained segment and motion state.
/// </summary>
public Av1FrameInfo.ReferenceState ReferenceState { get; } = referenceState;
/// <summary>
/// Gets the retained entropy snapshot and its return owner when one was published.
/// </summary>
public EntropyOwnership? Entropy { get; } = entropy;
}
/// <summary>
/// Pairs a retained entropy snapshot with the decoder-session owner that must receive it on release.
/// </summary>
private readonly struct EntropyOwnership(
Av1FrameEntropyContext context,
Av1FrameEntropyContexts owner)
{
/// <summary>
/// Gets the retained entropy snapshot.
/// </summary>
public Av1FrameEntropyContext Context { get; } = context;
/// <summary>
/// Gets the decoder-session owner that receives the snapshot.
/// </summary>
public Av1FrameEntropyContexts Owner { get; } = owner;
} }
} }

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

@ -24,7 +24,7 @@ internal static class Av1ReferenceFrameBorder
ExtendPlane( ExtendPlane(
frameBuffer, frameBuffer,
frameBuffer.BufferY!, frameBuffer.GetPlaneBuffer(Av1Plane.Y),
frameBuffer.OriginX, frameBuffer.OriginX,
frameBuffer.OriginY, frameBuffer.OriginY,
frameBuffer.Width, frameBuffer.Width,
@ -37,8 +37,8 @@ internal static class Av1ReferenceFrameBorder
int chromaOriginX = frameBuffer.OriginX >> subsamplingX; int chromaOriginX = frameBuffer.OriginX >> subsamplingX;
int chromaOriginY = frameBuffer.OriginY >> subsamplingY; int chromaOriginY = frameBuffer.OriginY >> subsamplingY;
ExtendPlane(frameBuffer, frameBuffer.BufferCb!, chromaOriginX, chromaOriginY, chromaWidth, chromaHeight); ExtendPlane(frameBuffer, frameBuffer.GetPlaneBuffer(Av1Plane.U), chromaOriginX, chromaOriginY, chromaWidth, chromaHeight);
ExtendPlane(frameBuffer, frameBuffer.BufferCr!, chromaOriginX, chromaOriginY, chromaWidth, chromaHeight); ExtendPlane(frameBuffer, frameBuffer.GetPlaneBuffer(Av1Plane.V), chromaOriginX, chromaOriginY, chromaWidth, chromaHeight);
} }
} }

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

@ -42,6 +42,27 @@ internal sealed class Av1ReferenceFrameStore : IDisposable
/// <returns>The retained frame, or <see langword="null"/> when the slot has not been populated.</returns> /// <returns>The retained frame, or <see langword="null"/> when the slot has not been populated.</returns>
public Av1ReferenceFrame? Resolve(int slot) => this.frames[slot]; public Av1ReferenceFrame? Resolve(int slot) => this.frames[slot];
/// <summary>
/// Resolves a reference-map slot that an earlier syntax boundary has established as occupied.
/// </summary>
/// <param name="slot">The zero-based reference-map slot.</param>
/// <returns>The retained frame in the selected slot.</returns>
public Av1ReferenceFrame ResolveRequired(int slot)
{
return this.Resolve(slot)
?? throw new InvalidImageContentException($"The AV1 reference-map slot {slot} has not been populated.");
}
/// <summary>
/// Resolves the presentation output established by the completed bounded payload.
/// </summary>
/// <returns>The retained frame selected for presentation.</returns>
public Av1ReferenceFrame ResolveOutput()
{
return this.outputFrame
?? throw new InvalidImageContentException("The AV1 payload did not produce a shown frame.");
}
/// <summary> /// <summary>
/// Writes whether each reference-map slot currently owns a reconstructed frame. /// Writes whether each reference-map slot currently owns a reconstructed frame.
/// </summary> /// </summary>
@ -164,7 +185,7 @@ internal sealed class Av1ReferenceFrameStore : IDisposable
/// <returns>The retained frame selected for presentation.</returns> /// <returns>The retained frame selected for presentation.</returns>
public Av1ReferenceFrame ShowExisting(int slot) public Av1ReferenceFrame ShowExisting(int slot)
{ {
Av1ReferenceFrame selectedFrame = this.frames[slot]!; Av1ReferenceFrame selectedFrame = this.ResolveRequired(slot);
Av1ReferenceFrame? replacedOutputFrame = this.outputFrame; Av1ReferenceFrame? replacedOutputFrame = this.outputFrame;
this.outputFrame = selectedFrame; this.outputFrame = selectedFrame;
@ -217,7 +238,7 @@ internal sealed class Av1ReferenceFrameStore : IDisposable
/// <returns>The selected presentation frame now owned by the caller.</returns> /// <returns>The selected presentation frame now owned by the caller.</returns>
public Av1ReferenceFrame TakeOutput() public Av1ReferenceFrame TakeOutput()
{ {
Av1ReferenceFrame result = this.outputFrame!; Av1ReferenceFrame result = this.ResolveOutput();
this.outputFrame = null; this.outputFrame = null;
// The caller becomes the sole owner of the selected output. Remove all slot aliases before Reset releases the // The caller becomes the sole owner of the selected output. Remove all slot aliases before Reset releases the

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

@ -58,12 +58,12 @@ internal struct Av1BlockModeInfo
/// <summary> /// <summary>
/// Stores the luma palette color-index map. /// Stores the luma palette color-index map.
/// </summary> /// </summary>
private Buffer2DRegion<byte> lumaPaletteColorIndexMap; private Rectangle lumaPaletteColorIndexBounds;
/// <summary> /// <summary>
/// Stores the shared chroma palette color-index map. /// Stores the shared chroma palette color-index map.
/// </summary> /// </summary>
private Buffer2DRegion<byte> chromaPaletteColorIndexMap; private Rectangle chromaPaletteColorIndexBounds;
/// <summary> /// <summary>
/// The directional prediction angle adjustment for luma. /// The directional prediction angle adjustment for luma.
@ -440,24 +440,27 @@ internal struct Av1BlockModeInfo
/// Gets the palette color-index map for a color plane. /// Gets the palette color-index map for a color plane.
/// </summary> /// </summary>
/// <param name="plane">The color plane.</param> /// <param name="plane">The color plane.</param>
/// <param name="colorIndexMap">The decoder-session palette map for the selected plane class.</param>
/// <returns>The luma map for <see cref="Av1Plane.Y"/> or the shared chroma map for either chroma plane.</returns> /// <returns>The luma map for <see cref="Av1Plane.Y"/> or the shared chroma map for either chroma plane.</returns>
public Buffer2DRegion<byte> GetPaletteColorIndexMap(Av1Plane plane) public Buffer2DRegion<byte> GetPaletteColorIndexMap(Av1Plane plane, Buffer2D<byte> colorIndexMap)
=> plane == Av1Plane.Y ? this.lumaPaletteColorIndexMap : this.chromaPaletteColorIndexMap; => new(
colorIndexMap,
plane == Av1Plane.Y ? this.lumaPaletteColorIndexBounds : this.chromaPaletteColorIndexBounds);
/// <summary> /// <summary>
/// Stores the palette color-index map for a plane class. /// Stores the palette color-index map for a plane class.
/// </summary> /// </summary>
/// <param name="planeType">The luma or shared chroma plane class.</param> /// <param name="planeType">The luma or shared chroma plane class.</param>
/// <param name="colorIndexMap">The row-major color-index map including coded-block edge padding.</param> /// <param name="bounds">The row-major color-index bounds including coded-block edge padding.</param>
public void SetPaletteColorIndexMap(Av1PlaneType planeType, Buffer2DRegion<byte> colorIndexMap) public void SetPaletteColorIndexMap(Av1PlaneType planeType, Rectangle bounds)
{ {
if (planeType == Av1PlaneType.Y) if (planeType == Av1PlaneType.Y)
{ {
this.lumaPaletteColorIndexMap = colorIndexMap; this.lumaPaletteColorIndexBounds = bounds;
} }
else else
{ {
this.chromaPaletteColorIndexMap = colorIndexMap; this.chromaPaletteColorIndexBounds = bounds;
} }
} }

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

@ -52,15 +52,20 @@ internal partial class Av1FrameInfo
/// <summary> /// <summary>
/// Owns the selected motion vector and logical reference for every retained 8x8 frame position. /// Owns the selected motion vector and logical reference for every retained 8x8 frame position.
/// </summary> /// </summary>
private IMemoryOwner<RetainedMotionFieldEntry>? retainedMotionField; private MotionFieldStorage<RetainedMotionFieldEntry>? retainedMotionField;
/// <summary> /// <summary>
/// Owns motion vectors projected from retained frames into the current frame's 8x8 grid. /// Owns motion vectors projected from retained frames into the current frame's 8x8 grid.
/// </summary> /// </summary>
private IMemoryOwner<TemporalMotionFieldEntry>? temporalMotionField; private MotionFieldStorage<TemporalMotionFieldEntry>? temporalMotionField;
/// <summary> /// <summary>
/// The number of tile-reader, reference-frame, and decoder-result owners retaining this frame state. /// The compact reference state detached from reconstruction storage after this frame refreshes the reference map.
/// </summary>
private ReferenceState? referenceState;
/// <summary>
/// The number of tile-reader and decoder-result owners retaining this reconstruction state.
/// </summary> /// </summary>
private int ownerCount = 1; private int ownerCount = 1;
@ -84,16 +89,6 @@ internal partial class Av1FrameInfo
/// </summary> /// </summary>
private uint motionFieldOrderHint; private uint motionFieldOrderHint;
/// <summary>
/// The number of retained motion-field entries in one active 8x8 row.
/// </summary>
private int retainedMotionFieldStride;
/// <summary>
/// The number of projected temporal-motion entries in one aligned 8x8 row.
/// </summary>
private int temporalMotionFieldStride;
/// <summary> /// <summary>
/// The active frame width in 4x4 mode-information units. /// The active frame width in 4x4 mode-information units.
/// </summary> /// </summary>
@ -125,7 +120,7 @@ internal partial class Av1FrameInfo
this.motionFieldOrderHint = frameHeader.OrderHint; this.motionFieldOrderHint = frameHeader.OrderHint;
this.activeModeInfoColumnCount = frameHeader.ModeInfoColumnCount; this.activeModeInfoColumnCount = frameHeader.ModeInfoColumnCount;
this.activeModeInfoRowCount = frameHeader.ModeInfoRowCount; this.activeModeInfoRowCount = frameHeader.ModeInfoRowCount;
this.retainedMotionFieldStride = (this.activeModeInfoColumnCount + 1) >> MotionFieldModeInfoShift; int retainedMotionFieldStride = (this.activeModeInfoColumnCount + 1) >> MotionFieldModeInfoShift;
if (frameHeader.IsIntra) if (frameHeader.IsIntra)
{ {
@ -134,8 +129,8 @@ internal partial class Av1FrameInfo
return; return;
} }
InlineArray8<Av1ReferenceFrame?> selectedReferences = default;
ReadOnlySpan<uint> referenceFrameIndices = frameHeader.GetReferenceFrameIndices(); ReadOnlySpan<uint> referenceFrameIndices = frameHeader.GetReferenceFrameIndices();
SelectedReferenceFrames selectedReferences = new(referenceFrames, referenceFrameIndices);
ObuOrderHintInfo orderHintInfo = sequenceHeader.OrderHintInfo; ObuOrderHintInfo orderHintInfo = sequenceHeader.OrderHintInfo;
// Capture the seven logical-role order hints before this frame refreshes any physical map slots. The reference decoder keeps // Capture the seven logical-role order hints before this frame refreshes any physical map slots. The reference decoder keeps
@ -143,9 +138,8 @@ internal partial class Av1FrameInfo
for (int referenceIndex = 0; referenceIndex < Av1Constants.ReferencesPerFrame; referenceIndex++) for (int referenceIndex = 0; referenceIndex < Av1Constants.ReferencesPerFrame; referenceIndex++)
{ {
Av1ReferenceFrameType referenceFrameType = (Av1ReferenceFrameType)(referenceIndex + 1); Av1ReferenceFrameType referenceFrameType = (Av1ReferenceFrameType)(referenceIndex + 1);
Av1ReferenceFrame referenceFrame = referenceFrames.Resolve((int)referenceFrameIndices[referenceIndex])!; Av1ReferenceFrame referenceFrame = selectedReferences[referenceFrameType];
uint referenceOrderHint = referenceFrame.FrameHeader.OrderHint; uint referenceOrderHint = referenceFrame.FrameHeader.OrderHint;
selectedReferences[(int)referenceFrameType] = referenceFrame;
this.motionFieldReferenceOrderHints[(int)referenceFrameType] = referenceOrderHint; this.motionFieldReferenceOrderHints[(int)referenceFrameType] = referenceOrderHint;
int relativeDistance = orderHintInfo.GetRelativeDistance(referenceOrderHint, frameHeader.OrderHint); int relativeDistance = orderHintInfo.GetRelativeDistance(referenceOrderHint, frameHeader.OrderHint);
@ -161,13 +155,16 @@ internal partial class Av1FrameInfo
return; return;
} }
// FrameInfo is shared directly by the tile reader, reference frames, and the final decoder result. The // The retained field transfers to compact reference state after reconstruction. Keeping it allocator-backed
// allocator-owned motion fields therefore follow that shared lifetime without placing frame-sized arrays on // avoids placing a frame-sized array on the managed heap. Clean storage is required because an all-zero entry
// the managed heap. Clean storage is required because an all-zero entry denotes the normative empty field. // denotes the normative empty field.
int retainedRowCount = (this.activeModeInfoRowCount + 1) >> MotionFieldModeInfoShift; int retainedRowCount = (this.activeModeInfoRowCount + 1) >> MotionFieldModeInfoShift;
this.retainedMotionField = configuration.MemoryAllocator.Allocate<RetainedMotionFieldEntry>( IMemoryOwner<RetainedMotionFieldEntry> retainedMotionFieldOwner =
this.retainedMotionFieldStride * retainedRowCount, configuration.MemoryAllocator.Allocate<RetainedMotionFieldEntry>(
AllocationOptions.Clean); retainedMotionFieldStride * retainedRowCount,
AllocationOptions.Clean);
this.retainedMotionField = new(retainedMotionFieldOwner, retainedMotionFieldStride);
if (!frameHeader.UseReferenceFrameMotionVectors) if (!frameHeader.UseReferenceFrameMotionVectors)
{ {
@ -180,57 +177,64 @@ internal partial class Av1FrameInfo
this.activeModeInfoColumnCount, this.activeModeInfoColumnCount,
MaximumSuperblockModeInfoSizeLog2); MaximumSuperblockModeInfoSizeLog2);
this.temporalMotionFieldStride = alignedModeInfoColumnCount >> MotionFieldModeInfoShift; int temporalMotionFieldStride = alignedModeInfoColumnCount >> MotionFieldModeInfoShift;
int temporalRowCount = (this.activeModeInfoRowCount + MaximumSuperblockModeInfoSize) >> MotionFieldModeInfoShift; int temporalRowCount = (this.activeModeInfoRowCount + MaximumSuperblockModeInfoSize) >> MotionFieldModeInfoShift;
this.temporalMotionField = configuration.MemoryAllocator.Allocate<TemporalMotionFieldEntry>( IMemoryOwner<TemporalMotionFieldEntry> temporalMotionFieldOwner =
this.temporalMotionFieldStride * temporalRowCount, configuration.MemoryAllocator.Allocate<TemporalMotionFieldEntry>(
AllocationOptions.Clean); temporalMotionFieldStride * temporalRowCount,
AllocationOptions.Clean);
MotionFieldStorage<TemporalMotionFieldEntry> temporalMotionField =
new(temporalMotionFieldOwner, temporalMotionFieldStride);
this.temporalMotionField = temporalMotionField;
// AV1 examines LAST, BWDREF, ALTREF2, ALTREF, and LAST2 in this normative order and admits at most three // AV1 examines LAST, BWDREF, ALTREF2, ALTREF, and LAST2 in this normative order and admits at most three
// projection sources. LAST always consumes the first budget position, forward references consume one only // projection sources. LAST always consumes the first budget position, forward references consume one only
// when eligible projection succeeds, and LAST2 fills the final unused position in the reverse direction. // when eligible projection succeeds, and LAST2 fills the final unused position in the reverse direction.
int remainingProjectionCount = MotionFieldProjectionCount; int remainingProjectionCount = MotionFieldProjectionCount;
Av1ReferenceFrame lastFrame = selectedReferences[(int)Av1ReferenceFrameType.Last]!; Av1ReferenceFrame lastFrame = selectedReferences[Av1ReferenceFrameType.Last];
Av1ReferenceFrame goldenFrame = selectedReferences[(int)Av1ReferenceFrameType.Golden]!; Av1ReferenceFrame goldenFrame = selectedReferences[Av1ReferenceFrameType.Golden];
uint alternateOfLastOrderHint = lastFrame.FrameInfo.motionFieldReferenceOrderHints[(int)Av1ReferenceFrameType.Alternate]; uint alternateOfLastOrderHint =
lastFrame.ReferenceState.MotionFieldReferenceOrderHints[(int)Av1ReferenceFrameType.Alternate];
// A LAST frame whose ALTREF order matches GOLDEN is an overlay. Projecting it would duplicate the overlay's // A LAST frame whose ALTREF order matches GOLDEN is an overlay. Projecting it would duplicate the overlay's
// temporal source, but the reference decoder still consumes one position from the three-source projection budget. // temporal source, but the reference decoder still consumes one position from the three-source projection budget.
if (alternateOfLastOrderHint != goldenFrame.FrameHeader.OrderHint) if (alternateOfLastOrderHint != goldenFrame.FrameHeader.OrderHint)
{ {
_ = this.ProjectMotionField(sequenceHeader, frameHeader, lastFrame, reverseDirection: true); _ = this.ProjectMotionField(sequenceHeader, frameHeader, lastFrame, temporalMotionField, reverseDirection: true);
} }
remainingProjectionCount--; remainingProjectionCount--;
Av1ReferenceFrame backwardFrame = selectedReferences[(int)Av1ReferenceFrameType.Backward]!; Av1ReferenceFrame backwardFrame = selectedReferences[Av1ReferenceFrameType.Backward];
if (orderHintInfo.GetRelativeDistance(backwardFrame.FrameHeader.OrderHint, frameHeader.OrderHint) > 0 && if (orderHintInfo.GetRelativeDistance(backwardFrame.FrameHeader.OrderHint, frameHeader.OrderHint) > 0 &&
this.ProjectMotionField(sequenceHeader, frameHeader, backwardFrame, reverseDirection: false)) this.ProjectMotionField(sequenceHeader, frameHeader, backwardFrame, temporalMotionField, reverseDirection: false))
{ {
remainingProjectionCount--; remainingProjectionCount--;
} }
Av1ReferenceFrame alternate2Frame = selectedReferences[(int)Av1ReferenceFrameType.Alternate2]!; Av1ReferenceFrame alternate2Frame = selectedReferences[Av1ReferenceFrameType.Alternate2];
if (orderHintInfo.GetRelativeDistance(alternate2Frame.FrameHeader.OrderHint, frameHeader.OrderHint) > 0 && if (orderHintInfo.GetRelativeDistance(alternate2Frame.FrameHeader.OrderHint, frameHeader.OrderHint) > 0 &&
this.ProjectMotionField(sequenceHeader, frameHeader, alternate2Frame, reverseDirection: false)) this.ProjectMotionField(sequenceHeader, frameHeader, alternate2Frame, temporalMotionField, reverseDirection: false))
{ {
remainingProjectionCount--; remainingProjectionCount--;
} }
Av1ReferenceFrame alternateFrame = selectedReferences[(int)Av1ReferenceFrameType.Alternate]!; Av1ReferenceFrame alternateFrame = selectedReferences[Av1ReferenceFrameType.Alternate];
if (remainingProjectionCount > 0 && if (remainingProjectionCount > 0 &&
orderHintInfo.GetRelativeDistance(alternateFrame.FrameHeader.OrderHint, frameHeader.OrderHint) > 0 && orderHintInfo.GetRelativeDistance(alternateFrame.FrameHeader.OrderHint, frameHeader.OrderHint) > 0 &&
this.ProjectMotionField(sequenceHeader, frameHeader, alternateFrame, reverseDirection: false)) this.ProjectMotionField(sequenceHeader, frameHeader, alternateFrame, temporalMotionField, reverseDirection: false))
{ {
remainingProjectionCount--; remainingProjectionCount--;
} }
if (remainingProjectionCount > 0) if (remainingProjectionCount > 0)
{ {
Av1ReferenceFrame last2Frame = selectedReferences[(int)Av1ReferenceFrameType.Last2]!; Av1ReferenceFrame last2Frame = selectedReferences[Av1ReferenceFrameType.Last2];
_ = this.ProjectMotionField(sequenceHeader, frameHeader, last2Frame, reverseDirection: true); _ = this.ProjectMotionField(sequenceHeader, frameHeader, last2Frame, temporalMotionField, reverseDirection: true);
} }
} }
@ -248,10 +252,19 @@ internal partial class Av1FrameInfo
out Av1MotionVector motionVector, out Av1MotionVector motionVector,
out int referenceFrameOffset) out int referenceFrameOffset)
{ {
int index = ((modeInfoRow >> MotionFieldModeInfoShift) * this.temporalMotionFieldStride) + MotionFieldStorage<TemporalMotionFieldEntry>? temporalMotionFieldState = this.temporalMotionField;
if (temporalMotionFieldState is null)
{
motionVector = default;
referenceFrameOffset = 0;
return false;
}
MotionFieldStorage<TemporalMotionFieldEntry> temporalMotionField = temporalMotionFieldState.Value;
int index = ((modeInfoRow >> MotionFieldModeInfoShift) * temporalMotionField.Stride) +
(modeInfoColumn >> MotionFieldModeInfoShift); (modeInfoColumn >> MotionFieldModeInfoShift);
TemporalMotionFieldEntry entry = this.temporalMotionField!.Memory.Span[index]; TemporalMotionFieldEntry entry = temporalMotionField.Owner.Memory.Span[index];
motionVector = entry.MotionVector; motionVector = entry.MotionVector;
referenceFrameOffset = entry.ReferenceFrameOffset; referenceFrameOffset = entry.ReferenceFrameOffset;
return referenceFrameOffset > 0; return referenceFrameOffset > 0;
@ -314,13 +327,14 @@ internal partial class Av1FrameInfo
/// <param name="modeInfoPosition">The block origin in frame-relative 4x4 units.</param> /// <param name="modeInfoPosition">The block origin in frame-relative 4x4 units.</param>
private void UpdateRetainedMotionField(Av1BlockModeInfo modeInfo, Point modeInfoPosition) private void UpdateRetainedMotionField(Av1BlockModeInfo modeInfo, Point modeInfoPosition)
{ {
IMemoryOwner<RetainedMotionFieldEntry>? retainedMotionField = this.retainedMotionField; MotionFieldStorage<RetainedMotionFieldEntry>? retainedMotionFieldState = this.retainedMotionField;
if (retainedMotionField is null) if (retainedMotionFieldState is null)
{ {
return; return;
} }
Span<RetainedMotionFieldEntry> retainedEntries = retainedMotionField.Memory.Span; MotionFieldStorage<RetainedMotionFieldEntry> retainedMotionField = retainedMotionFieldState.Value;
Span<RetainedMotionFieldEntry> retainedEntries = retainedMotionField.Owner.Memory.Span;
Av1ReferenceFrameType selectedReference = Av1ReferenceFrameType.None; Av1ReferenceFrameType selectedReference = Av1ReferenceFrameType.None;
Av1MotionVector selectedMotionVector = default; Av1MotionVector selectedMotionVector = default;
@ -363,7 +377,7 @@ internal partial class Av1FrameInfo
// normative ability to overwrite the shared cell in traversal order. // normative ability to overwrite the shared cell in traversal order.
for (int row = 0; row < fieldHeight; row++) for (int row = 0; row < fieldHeight; row++)
{ {
int rowOffset = ((firstFieldRow + row) * this.retainedMotionFieldStride) + firstFieldColumn; int rowOffset = ((firstFieldRow + row) * retainedMotionField.Stride) + firstFieldColumn;
retainedEntries.Slice(rowOffset, fieldWidth).Fill(entry); retainedEntries.Slice(rowOffset, fieldWidth).Fill(entry);
} }
} }
@ -374,6 +388,7 @@ internal partial class Av1FrameInfo
/// <param name="sequenceHeader">The sequence header defining the modulo order-hint domain.</param> /// <param name="sequenceHeader">The sequence header defining the modulo order-hint domain.</param>
/// <param name="frameHeader">The current frame header.</param> /// <param name="frameHeader">The current frame header.</param>
/// <param name="startFrame">The retained frame whose stored motion vectors are projected.</param> /// <param name="startFrame">The retained frame whose stored motion vectors are projected.</param>
/// <param name="temporalMotionField">The complete destination field storage for the current frame.</param>
/// <param name="reverseDirection"> /// <param name="reverseDirection">
/// A value indicating whether the start-to-current distance and spatial displacement are reversed for a past frame. /// A value indicating whether the start-to-current distance and spatial displacement are reversed for a past frame.
/// </param> /// </param>
@ -382,6 +397,7 @@ internal partial class Av1FrameInfo
ObuSequenceHeader sequenceHeader, ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader, ObuFrameHeader frameHeader,
Av1ReferenceFrame startFrame, Av1ReferenceFrame startFrame,
MotionFieldStorage<TemporalMotionFieldEntry> temporalMotionField,
bool reverseDirection) bool reverseDirection)
{ {
ObuFrameHeader startFrameHeader = startFrame.FrameHeader; ObuFrameHeader startFrameHeader = startFrame.FrameHeader;
@ -394,7 +410,7 @@ internal partial class Av1FrameInfo
return false; return false;
} }
Av1FrameInfo startFrameInfo = startFrame.FrameInfo; ReferenceState startFrameState = startFrame.ReferenceState;
ObuOrderHintInfo orderHintInfo = sequenceHeader.OrderHintInfo; ObuOrderHintInfo orderHintInfo = sequenceHeader.OrderHintInfo;
int startToCurrentFrameOffset = orderHintInfo.GetRelativeDistance( int startToCurrentFrameOffset = orderHintInfo.GetRelativeDistance(
startFrameHeader.OrderHint, startFrameHeader.OrderHint,
@ -409,12 +425,20 @@ internal partial class Av1FrameInfo
int sourceColumnCount = (this.activeModeInfoColumnCount + 1) >> MotionFieldModeInfoShift; int sourceColumnCount = (this.activeModeInfoColumnCount + 1) >> MotionFieldModeInfoShift;
int destinationRowCount = this.activeModeInfoRowCount >> MotionFieldModeInfoShift; int destinationRowCount = this.activeModeInfoRowCount >> MotionFieldModeInfoShift;
int destinationColumnCount = this.activeModeInfoColumnCount >> MotionFieldModeInfoShift; int destinationColumnCount = this.activeModeInfoColumnCount >> MotionFieldModeInfoShift;
ReadOnlySpan<RetainedMotionFieldEntry> sourceEntries = startFrameInfo.retainedMotionField!.Memory.Span; MotionFieldStorage<RetainedMotionFieldEntry>? retainedMotionFieldState = startFrameState.RetainedMotionField;
Span<TemporalMotionFieldEntry> destinationEntries = this.temporalMotionField!.Memory.Span; if (retainedMotionFieldState is null)
{
return false;
}
MotionFieldStorage<RetainedMotionFieldEntry> retainedMotionField = retainedMotionFieldState.Value;
ReadOnlySpan<RetainedMotionFieldEntry> sourceEntries = retainedMotionField.Owner.Memory.Span;
Span<TemporalMotionFieldEntry> destinationEntries = temporalMotionField.Owner.Memory.Span;
for (int blockRow = 0; blockRow < sourceRowCount; blockRow++) for (int blockRow = 0; blockRow < sourceRowCount; blockRow++)
{ {
int sourceRowOffset = blockRow * startFrameInfo.retainedMotionFieldStride; int sourceRowOffset = blockRow * retainedMotionField.Stride;
for (int blockColumn = 0; blockColumn < sourceColumnCount; blockColumn++) for (int blockColumn = 0; blockColumn < sourceColumnCount; blockColumn++)
{ {
RetainedMotionFieldEntry source = sourceEntries[sourceRowOffset + blockColumn]; RetainedMotionFieldEntry source = sourceEntries[sourceRowOffset + blockColumn];
@ -425,7 +449,7 @@ internal partial class Av1FrameInfo
int referenceFrameOffset = orderHintInfo.GetRelativeDistance( int referenceFrameOffset = orderHintInfo.GetRelativeDistance(
startFrameHeader.OrderHint, startFrameHeader.OrderHint,
startFrameInfo.motionFieldReferenceOrderHints[(int)source.ReferenceFrame]); startFrameState.MotionFieldReferenceOrderHints[(int)source.ReferenceFrame]);
bool positionIsValid = Math.Abs(referenceFrameOffset) <= Av1MotionVector.MaximumTemporalDistance && bool positionIsValid = Math.Abs(referenceFrameOffset) <= Av1MotionVector.MaximumTemporalDistance &&
referenceFrameOffset > 0 && referenceFrameOffset > 0 &&
@ -453,7 +477,7 @@ internal partial class Av1FrameInfo
// The projected vector selects the destination cell, but AV1 stores the original forward vector and // The projected vector selects the destination cell, but AV1 stores the original forward vector and
// its source-to-reference distance there. Candidate scaling later uses both values for its own target. // its source-to-reference distance there. Candidate scaling later uses both values for its own target.
int destinationOffset = (projectedRow * this.temporalMotionFieldStride) + projectedColumn; int destinationOffset = (projectedRow * temporalMotionField.Stride) + projectedColumn;
destinationEntries[destinationOffset] = new(source.MotionVector, referenceFrameOffset); destinationEntries[destinationOffset] = new(source.MotionVector, referenceFrameOffset);
} }
} }
@ -461,6 +485,43 @@ internal partial class Av1FrameInfo
return true; return true;
} }
/// <summary>
/// Detaches the compact state required while this decoded frame occupies the reference map.
/// </summary>
public ReferenceState PrepareReferenceState()
{
ReferenceState? state = this.referenceState;
if (state is null)
{
// libaom's RefCntBuffer retains only the segment map, 8x8 motion field, and reference-order hints from
// block reconstruction. Transfer those owners without copying and leave all other frame syntax local.
state = new ReferenceState(
this.segmentIds,
this.segmentIdColumnCount,
this.segmentIdRowCount,
this.retainedMotionField,
this.motionFieldReferenceOrderHints);
this.segmentIds = null;
this.retainedMotionField = null;
this.referenceState = state;
}
return state;
}
/// <summary>
/// Acquires the prepared compact state for one reference-frame owner.
/// </summary>
/// <returns>The retained reference state with one ownership lease for the caller.</returns>
public ReferenceState AcquireReferenceState()
{
ReferenceState state = this.PrepareReferenceState();
state.AddOwner();
return state;
}
/// <summary> /// <summary>
/// Adds one owner for this frame state. /// Adds one owner for this frame state.
/// </summary> /// </summary>
@ -493,12 +554,29 @@ internal partial class Av1FrameInfo
this.ownerCount--; this.ownerCount--;
if (this.ownerCount == 0) if (this.ownerCount == 0)
{ {
// Frame-sized motion storage remains addressable through retained references. Return it only after tile, // Reconstruction-only syntax expires with the tile reader and optional decoder-result owner. A detached
// reference, presentation, and decoder-result owners are gone. // reference state has its own lease and can outlive this full frame state.
this.referenceState?.ReleaseOwner();
this.referenceState = null;
this.retainedMotionField?.Dispose(); this.retainedMotionField?.Dispose();
this.retainedMotionField = null; this.retainedMotionField = null;
this.temporalMotionField?.Dispose(); this.temporalMotionField?.Dispose();
this.temporalMotionField = null; this.temporalMotionField = null;
for (int plane = 0; plane < Av1Constants.MaxPlanes; plane++)
{
this.loopRestorationUnits[plane]?.Dispose();
}
this.segmentIds?.Dispose();
this.coefficientScratch.Dispose();
this.deltaLoopFilter.Dispose();
this.cdefStrength.Dispose();
this.quantizerIndices.Dispose();
this.transformInfoScratch.Dispose();
this.modeInfoMap.Dispose();
this.modeInfoCounts.Dispose();
this.modeInfos.Dispose();
} }
} }
@ -547,10 +625,111 @@ internal partial class Av1FrameInfo
projectedColumn < baseBlockColumn + 8 + MaximumHorizontalFieldOffset; projectedColumn < baseBlockColumn + 8 + MaximumHorizontalFieldOffset;
} }
/// <summary>
/// Couples one allocator-owned motion-field buffer with the stride required to address it.
/// </summary>
internal readonly struct MotionFieldStorage<T>
where T : struct
{
/// <summary>
/// Initializes a new instance of the <see cref="MotionFieldStorage{T}"/> struct.
/// </summary>
public MotionFieldStorage(IMemoryOwner<T> owner, int stride)
{
this.Owner = owner;
this.Stride = stride;
}
/// <summary>
/// Gets the allocator-owned field entries.
/// </summary>
public IMemoryOwner<T> Owner { get; }
/// <summary>
/// Gets the number of entries in one field row.
/// </summary>
public int Stride { get; }
/// <summary>
/// Returns the field entries to their allocator.
/// </summary>
public void Dispose() => this.Owner.Dispose();
}
/// <summary>
/// Carries the complete seven-role reference mapping captured before the current frame refreshes map slots.
/// </summary>
private readonly struct SelectedReferenceFrames
{
/// <summary>
/// Initializes a new instance of the <see cref="SelectedReferenceFrames"/> struct.
/// </summary>
public SelectedReferenceFrames(Av1ReferenceFrameStore referenceFrames, ReadOnlySpan<uint> referenceFrameIndices)
{
this.Last = referenceFrames.ResolveRequired((int)referenceFrameIndices[0]);
this.Last2 = referenceFrames.ResolveRequired((int)referenceFrameIndices[1]);
this.Last3 = referenceFrames.ResolveRequired((int)referenceFrameIndices[2]);
this.Golden = referenceFrames.ResolveRequired((int)referenceFrameIndices[3]);
this.Backward = referenceFrames.ResolveRequired((int)referenceFrameIndices[4]);
this.Alternate2 = referenceFrames.ResolveRequired((int)referenceFrameIndices[5]);
this.Alternate = referenceFrames.ResolveRequired((int)referenceFrameIndices[6]);
}
/// <summary>
/// Gets the retained LAST frame.
/// </summary>
public Av1ReferenceFrame Last { get; }
/// <summary>
/// Gets the retained LAST2 frame.
/// </summary>
public Av1ReferenceFrame Last2 { get; }
/// <summary>
/// Gets the retained LAST3 frame.
/// </summary>
public Av1ReferenceFrame Last3 { get; }
/// <summary>
/// Gets the retained GOLDEN frame.
/// </summary>
public Av1ReferenceFrame Golden { get; }
/// <summary>
/// Gets the retained BWDREF frame.
/// </summary>
public Av1ReferenceFrame Backward { get; }
/// <summary>
/// Gets the retained ALTREF2 frame.
/// </summary>
public Av1ReferenceFrame Alternate2 { get; }
/// <summary>
/// Gets the retained ALTREF frame.
/// </summary>
public Av1ReferenceFrame Alternate { get; }
/// <summary>
/// Gets the retained frame for one canonical inter-reference role.
/// </summary>
public Av1ReferenceFrame this[Av1ReferenceFrameType referenceFrame] => referenceFrame switch
{
Av1ReferenceFrameType.Last => this.Last,
Av1ReferenceFrameType.Last2 => this.Last2,
Av1ReferenceFrameType.Last3 => this.Last3,
Av1ReferenceFrameType.Golden => this.Golden,
Av1ReferenceFrameType.Backward => this.Backward,
Av1ReferenceFrameType.Alternate2 => this.Alternate2,
Av1ReferenceFrameType.Alternate => this.Alternate,
_ => throw new InvalidOperationException("Motion fields only use canonical inter-reference roles.")
};
}
/// <summary> /// <summary>
/// Stores one motion vector and logical reference retained for projection by a later frame. /// Stores one motion vector and logical reference retained for projection by a later frame.
/// </summary> /// </summary>
private readonly struct RetainedMotionFieldEntry internal readonly struct RetainedMotionFieldEntry
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RetainedMotionFieldEntry"/> struct. /// Initializes a new instance of the <see cref="RetainedMotionFieldEntry"/> struct.
@ -600,4 +779,77 @@ internal partial class Av1FrameInfo
/// </summary> /// </summary>
public int ReferenceFrameOffset { get; } public int ReferenceFrameOffset { get; }
} }
/// <summary>
/// Owns only the per-frame syntax retained by libaom's reference buffer after reconstruction completes.
/// </summary>
internal sealed class ReferenceState
{
/// <summary>
/// The number of frame-info and reference-frame owners retaining this state.
/// </summary>
private int ownerCount = 1;
/// <summary>
/// Initializes a new instance of the <see cref="ReferenceState"/> class by taking ownership of retained buffers.
/// </summary>
public ReferenceState(
Buffer2D<byte>? segmentIds,
int segmentIdColumnCount,
int segmentIdRowCount,
MotionFieldStorage<RetainedMotionFieldEntry>? retainedMotionField,
InlineArray8<uint> motionFieldReferenceOrderHints)
{
this.SegmentIds = segmentIds;
this.SegmentIdColumnCount = segmentIdColumnCount;
this.SegmentIdRowCount = segmentIdRowCount;
this.RetainedMotionField = retainedMotionField;
this.MotionFieldReferenceOrderHints = motionFieldReferenceOrderHints;
}
/// <summary>
/// Gets the retained 4x4 segmentation map, or <see langword="null"/> when segmentation is disabled.
/// </summary>
public Buffer2D<byte>? SegmentIds { get; private set; }
/// <summary>
/// Gets the number of active 4x4 columns in <see cref="SegmentIds"/>.
/// </summary>
public int SegmentIdColumnCount { get; }
/// <summary>
/// Gets the number of active 4x4 rows in <see cref="SegmentIds"/>.
/// </summary>
public int SegmentIdRowCount { get; }
/// <summary>
/// Gets the retained per-8x8 motion field, or <see langword="null"/> when the temporal tool is disabled.
/// </summary>
public MotionFieldStorage<RetainedMotionFieldEntry>? RetainedMotionField { get; private set; }
/// <summary>
/// Gets the order hints selected by this frame's seven logical inter-reference roles.
/// </summary>
public InlineArray8<uint> MotionFieldReferenceOrderHints { get; }
/// <summary>
/// Adds one owner for this retained state.
/// </summary>
public void AddOwner() => this.ownerCount++;
/// <summary>
/// Releases one owner and returns retained buffers after the final lease.
/// </summary>
public void ReleaseOwner()
{
this.ownerCount--;
if (this.ownerCount == 0)
{
this.RetainedMotionField?.Dispose();
this.RetainedMotionField = null;
this.SegmentIds?.Dispose();
this.SegmentIds = null;
}
}
}
} }

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

@ -1,35 +1,106 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// <summary>
/// Identifies the inter-prediction features selected by decoded AV1 block syntax.
/// </summary>
[Flags]
internal enum Av1InterPredictionFeatures
{
/// <summary>
/// No tracked inter-prediction feature was selected.
/// </summary>
None = 0,
/// <summary>
/// Distance-weighted compound prediction was selected.
/// </summary>
DistanceWeightedCompound = 1 << 0,
/// <summary>
/// A non-inverted wedge compound mask was selected.
/// </summary>
WedgeCompound = 1 << 1,
/// <summary>
/// An inverted wedge compound mask was selected.
/// </summary>
InvertedWedgeCompound = 1 << 2,
/// <summary>
/// The first difference-weighted compound mask orientation was selected.
/// </summary>
DifferenceWeightedCompound = 1 << 3,
/// <summary>
/// The inverted difference-weighted compound mask orientation was selected.
/// </summary>
InvertedDifferenceWeightedCompound = 1 << 4,
/// <summary>
/// Smooth inter-intra prediction was selected.
/// </summary>
SmoothInterIntra = 1 << 5,
/// <summary>
/// Wedge inter-intra prediction was selected.
/// </summary>
WedgeInterIntra = 1 << 6,
/// <summary>
/// Overlapping motion compensation was selected.
/// </summary>
Obmc = 1 << 7,
/// <summary>
/// Local warped-motion prediction was selected.
/// </summary>
LocalWarp = 1 << 8,
/// <summary>
/// Non-translational global warped-motion prediction was selected.
/// </summary>
GlobalWarp = 1 << 9
}
/// <summary> /// <summary>
/// Owns the mode, motion, segmentation, transform, coefficient, quantizer, and filter state decoded for one AV1 frame. /// Owns the mode, motion, segmentation, transform, coefficient, quantizer, and filter state decoded for one AV1 frame.
/// </summary> /// </summary>
internal partial class Av1FrameInfo : IDisposable internal sealed partial class Av1FrameInfo : IDisposable
{ {
/// <summary>
/// The allocator that owns frame-sized syntax and retained-reference state.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary> /// <summary>
/// The coefficient slots reserved for one 4x4 mode-information unit: one end index followed by 16 coefficients. /// The coefficient slots reserved for one 4x4 mode-information unit: one end index followed by 16 coefficients.
/// </summary> /// </summary>
public const int CoefficientCountPerModeInfo = 1 + 16; public const int CoefficientCountPerModeInfo = 1 + 16;
/// <summary> /// <summary>
/// Stores raster-ordered luma coefficients for every frame superblock. /// Owns the luma and chroma coefficient scratch for the superblock currently being decoded.
/// </summary> /// </summary>
private readonly int[] coefficientsY = []; private readonly IMemoryOwner<int> coefficientScratch;
/// <summary> /// <summary>
/// Stores raster-ordered blue-difference chroma coefficients for every frame superblock. /// The number of luma coefficient entries at the start of <see cref="coefficientScratch"/>.
/// </summary> /// </summary>
private readonly int[] coefficientsU = []; private readonly int lumaCoefficientCount;
/// <summary> /// <summary>
/// Stores raster-ordered red-difference chroma coefficients for every frame superblock. /// The number of coefficient entries reserved for each chroma plane.
/// </summary> /// </summary>
private readonly int[] coefficientsV = []; private readonly int chromaCoefficientCount;
/// <summary> /// <summary>
/// The width and height of a superblock in 4x4 mode-information units. /// The width and height of a superblock in 4x4 mode-information units.
@ -57,14 +128,14 @@ internal partial class Av1FrameInfo : IDisposable
private readonly int subsamplingFactor; private readonly int subsamplingFactor;
/// <summary> /// <summary>
/// Stores one addressing view for each frame superblock. /// Stores decoded block mode information in bitstream traversal order.
/// </summary> /// </summary>
private readonly Av1SuperblockInfo[] superblockInfos; private readonly MemoryGroup<Av1BlockModeInfo> modeInfos;
/// <summary> /// <summary>
/// Stores decoded block mode information in bitstream traversal order. /// Stores the number of mode-information records written to each superblock row in <see cref="modeInfos"/>.
/// </summary> /// </summary>
private readonly Av1BlockModeInfo[] modeInfos; private readonly Buffer2D<int> modeInfoCounts;
/// <summary> /// <summary>
/// Maps every frame-relative 4x4 position to its covering entry in <see cref="modeInfos"/>. /// Maps every frame-relative 4x4 position to its covering entry in <see cref="modeInfos"/>.
@ -74,7 +145,7 @@ internal partial class Av1FrameInfo : IDisposable
/// <summary> /// <summary>
/// Stores the decoded segment identifier for each active 4x4 mode-information position in row-major order. /// Stores the decoded segment identifier for each active 4x4 mode-information position in row-major order.
/// </summary> /// </summary>
private byte[] segmentIds = []; private Buffer2D<byte>? segmentIds;
/// <summary> /// <summary>
/// The number of active 4x4 columns in one row of <see cref="segmentIds"/>. /// The number of active 4x4 columns in one row of <see cref="segmentIds"/>.
@ -87,19 +158,14 @@ internal partial class Av1FrameInfo : IDisposable
private int segmentIdRowCount; private int segmentIdRowCount;
/// <summary> /// <summary>
/// Stores luma transform information grouped by superblock. /// Owns the luma and shared-chroma transform-information scratch for the superblock currently being decoded.
/// </summary>
private readonly Av1TransformInfo[] transformInfosY;
/// <summary>
/// Stores both chroma planes' transform information grouped by superblock.
/// </summary> /// </summary>
private readonly Av1TransformInfo[] transformInfosUv; private readonly IMemoryOwner<Av1TransformInfo> transformInfoScratch;
/// <summary> /// <summary>
/// Stores the active base quantizer index for each frame superblock. /// Stores the active base quantizer index for each frame superblock.
/// </summary> /// </summary>
private readonly int[] quantizerIndices; private readonly Buffer2D<int> quantizerIndices;
/// <summary> /// <summary>
/// The base-2 number of constrained directional enhancement filter entries allocated per superblock. /// The base-2 number of constrained directional enhancement filter entries allocated per superblock.
@ -109,7 +175,7 @@ internal partial class Av1FrameInfo : IDisposable
/// <summary> /// <summary>
/// Stores constrained directional enhancement filter strengths grouped by superblock. /// Stores constrained directional enhancement filter strengths grouped by superblock.
/// </summary> /// </summary>
private readonly int[] cdefStrength; private readonly Buffer2D<int> cdefStrength;
/// <summary> /// <summary>
/// The base-2 number of loop-filter delta values stored per superblock. /// The base-2 number of loop-filter delta values stored per superblock.
@ -119,99 +185,259 @@ internal partial class Av1FrameInfo : IDisposable
/// <summary> /// <summary>
/// Stores the four loop-filter delta values for each superblock. /// Stores the four loop-filter delta values for each superblock.
/// </summary> /// </summary>
private readonly int[] deltaLoopFilter; private readonly Buffer2D<int> deltaLoopFilter;
/// <summary> /// <summary>
/// Stores raster-ordered loop-restoration units for each color plane. /// Stores raster-ordered loop-restoration units for each color plane.
/// </summary> /// </summary>
private readonly Av1LoopRestorationUnit[][] loopRestorationUnits = [[], [], []]; private InlineArray4<Buffer2D<Av1LoopRestorationUnit>?> loopRestorationUnits;
/// <summary> /// <summary>
/// Stores the number of loop-restoration unit columns for each color plane. /// Stores the number of loop-restoration unit columns for each color plane.
/// </summary> /// </summary>
private readonly int[] loopRestorationUnitColumns = new int[Av1Constants.MaxPlanes]; private InlineArray4<int> loopRestorationUnitColumns;
/// <summary> /// <summary>
/// The number of loop-restoration unit rows allocated for each color plane. /// The number of loop-restoration unit rows allocated for each color plane.
/// </summary> /// </summary>
private readonly int[] loopRestorationUnitRows = new int[Av1Constants.MaxPlanes]; private InlineArray4<int> loopRestorationUnitRows;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1FrameInfo"/> class. /// Initializes a new instance of the <see cref="Av1FrameInfo"/> class using sequence-maximum dimensions.
/// </summary> /// </summary>
/// <param name="sequenceHeader">The sequence header defining maximum dimensions, superblock size, and color sampling.</param> /// <param name="sequenceHeader">The sequence header defining maximum dimensions, superblock size, and color sampling.</param>
public Av1FrameInfo(ObuSequenceHeader sequenceHeader) public Av1FrameInfo(ObuSequenceHeader sequenceHeader)
: this(Configuration.Default, sequenceHeader, sequenceHeader.MaxFrameWidth, sequenceHeader.MaxFrameHeight)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameInfo"/> class for one active coded frame.
/// </summary>
/// <param name="sequenceHeader">The sequence header defining superblock size and color sampling.</param>
/// <param name="frameHeader">The frame header defining the active coded dimensions.</param>
public Av1FrameInfo(ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader)
: this(
Configuration.Default,
sequenceHeader,
frameHeader.FrameSize.FrameWidth,
frameHeader.FrameSize.FrameHeight)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameInfo"/> class for one active coded frame.
/// </summary>
/// <param name="configuration">The decoder configuration providing frame-sized storage.</param>
/// <param name="sequenceHeader">The sequence header defining superblock size and color sampling.</param>
/// <param name="frameHeader">The frame header defining the active coded dimensions.</param>
public Av1FrameInfo(Configuration configuration, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader)
: this(
configuration,
sequenceHeader,
frameHeader.FrameSize.FrameWidth,
frameHeader.FrameSize.FrameHeight)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameInfo"/> class for explicit coded dimensions.
/// </summary>
/// <param name="configuration">The decoder configuration providing frame-sized storage.</param>
/// <param name="sequenceHeader">The sequence header defining superblock size and color sampling.</param>
/// <param name="frameWidth">The active coded frame width.</param>
/// <param name="frameHeight">The active coded frame height.</param>
private Av1FrameInfo(Configuration configuration, ObuSequenceHeader sequenceHeader, int frameWidth, int frameHeight)
{ {
// Size frame-owned storage from the sequence maximums because later frame headers may select // A FrameInfo instance belongs to one coded frame, so transient syntax storage follows that frame rather
// any coded dimensions up to these bounds without rebuilding the decoder's indexing model. // than the potentially much larger sequence maximum declared by an untrusted stream.
this.memoryAllocator = configuration.MemoryAllocator;
int superblockSizeLog2 = sequenceHeader.SuperblockSizeLog2; int superblockSizeLog2 = sequenceHeader.SuperblockSizeLog2;
int superblockAlignedWidth = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, superblockSizeLog2); int superblockAlignedWidth = Av1Math.AlignPowerOf2(frameWidth, superblockSizeLog2);
int superblockAlignedHeight = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameHeight, superblockSizeLog2); int superblockAlignedHeight = Av1Math.AlignPowerOf2(frameHeight, superblockSizeLog2);
this.superblockColumnCount = superblockAlignedWidth >> superblockSizeLog2; this.superblockColumnCount = superblockAlignedWidth >> superblockSizeLog2;
this.superblockRowCount = superblockAlignedHeight >> superblockSizeLog2; this.superblockRowCount = superblockAlignedHeight >> superblockSizeLog2;
int superblockCount = this.superblockColumnCount * this.superblockRowCount; int superblockCount = this.superblockColumnCount * this.superblockRowCount;
this.modeInfoSizePerSuperblock = 1 << (superblockSizeLog2 - Av1Constants.ModeInfoSizeLog2); this.modeInfoSizePerSuperblock = 1 << (superblockSizeLog2 - Av1Constants.ModeInfoSizeLog2);
this.modeInfoCountPerSuperblock = this.modeInfoSizePerSuperblock * this.modeInfoSizePerSuperblock; this.modeInfoCountPerSuperblock = this.modeInfoSizePerSuperblock * this.modeInfoSizePerSuperblock;
int numPlanes = sequenceHeader.ColorConfig.IsMonochrome ? 1 : Av1Constants.MaxPlanes;
// A decoded block can cover multiple 4x4 positions, so modeInfos stores each block once while
// modeInfoMap makes every covered position resolve to that single traversal-order entry.
this.superblockInfos = new Av1SuperblockInfo[superblockCount];
this.modeInfos = new Av1BlockModeInfo[superblockCount * this.modeInfoCountPerSuperblock];
this.modeInfoMap = new Av1FrameModeInfoMap(new Size(this.modeInfoSizePerSuperblock * this.superblockColumnCount, this.modeInfoSizePerSuperblock * this.superblockRowCount));
this.transformInfosY = new Av1TransformInfo[superblockCount * this.modeInfoCountPerSuperblock];
this.transformInfosUv = new Av1TransformInfo[2 * superblockCount * this.modeInfoCountPerSuperblock];
// Superblock views retain only their grid position and address all storage through this owner.
int i = 0;
for (int y = 0; y < this.superblockRowCount; y++)
{
for (int x = 0; x < this.superblockColumnCount; x++)
{
Point point = new(x, y);
this.superblockInfos[i] = new(this, point);
i++;
}
}
bool subX = sequenceHeader.ColorConfig.SubSamplingX; bool subX = sequenceHeader.ColorConfig.SubSamplingX;
bool subY = sequenceHeader.ColorConfig.SubSamplingY; bool subY = sequenceHeader.ColorConfig.SubSamplingY;
// Chroma capacity scales by two for each sampled axis: 4:4:4 => 0, 4:2:2 => 1, 4:2:0 => 2. // Chroma capacity scales by two for each sampled axis: 4:4:4 => 0, 4:2:2 => 1, 4:2:0 => 2.
this.subsamplingFactor = (subX && subY) ? 2 : (subX && !subY) ? 1 : (!subX && !subY) ? 0 : -1; this.subsamplingFactor = (subX && subY) ? 2 : (subX && !subY) ? 1 : (!subX && !subY) ? 0 : -1;
Guard.IsFalse(this.subsamplingFactor == -1, nameof(this.subsamplingFactor), "Invalid combination of subsampling."); Guard.IsFalse(this.subsamplingFactor == -1, nameof(this.subsamplingFactor), "Invalid combination of subsampling.");
int lumaCoefficientCountPerSuperblock = this.modeInfoCountPerSuperblock * CoefficientCountPerModeInfo; this.lumaCoefficientCount = this.modeInfoCountPerSuperblock * CoefficientCountPerModeInfo;
int chromaCoefficientCountPerSuperblock = lumaCoefficientCountPerSuperblock >> this.subsamplingFactor; this.chromaCoefficientCount = sequenceHeader.ColorConfig.IsMonochrome
this.coefficientsY = new int[superblockCount * lumaCoefficientCountPerSuperblock]; ? 0
this.coefficientsU = new int[superblockCount * chromaCoefficientCountPerSuperblock]; : this.lumaCoefficientCount >> this.subsamplingFactor;
this.coefficientsV = new int[superblockCount * chromaCoefficientCountPerSuperblock];
this.quantizerIndices = new int[superblockCount]; IMemoryOwner<int>? allocatedCoefficientScratch = null;
MemoryGroup<Av1BlockModeInfo>? allocatedModeInfos = null;
// A 128x128 superblock contains four 64x64 CDEF filter blocks; a 64x64 superblock contains one. Buffer2D<int>? allocatedModeInfoCounts = null;
this.cdefStrengthFactorLog2 = (superblockSizeLog2 - 6) << 1; Av1FrameModeInfoMap? allocatedModeInfoMap = null;
this.cdefStrength = new int[superblockCount << this.cdefStrengthFactorLog2]; IMemoryOwner<Av1TransformInfo>? allocatedTransformInfoScratch = null;
Array.Fill(this.cdefStrength, -1); Buffer2D<int>? allocatedQuantizerIndices = null;
this.deltaLoopFilter = new int[superblockCount << this.deltaLoopFactorLog2]; Buffer2D<int>? allocatedCdefStrength = null;
Buffer2D<int>? allocatedDeltaLoopFilter = null;
try
{
// Reconstruction consumes one superblock before parsing the next. One allocator-owned scratch surface
// therefore covers all three planes without per-block arrays or unmanaged ownership outside ImageSharp.
int coefficientScratchLength = checked(this.lumaCoefficientCount + (2 * this.chromaCoefficientCount));
allocatedCoefficientScratch = this.memoryAllocator.Allocate<int>(coefficientScratchLength, AllocationOptions.Clean);
// A decoded block can cover multiple 4x4 positions. Each allocator-backed row stores one
// superblock's traversal records, while the map resolves every covered 4x4 position to them.
AllocationOptions clean = AllocationOptions.Clean;
// Mode information survives until frame completion, but one superblock row is much larger than a
// constrained allocator segment. Store it as a discontiguous group and address records by packed index.
long modeInfoLength = (long)this.modeInfoCountPerSuperblock * superblockCount;
allocatedModeInfos = this.memoryAllocator.AllocateGroup<Av1BlockModeInfo>(modeInfoLength, 1, clean);
allocatedModeInfoCounts = this.memoryAllocator.Allocate2D<int>(1, superblockCount, clean);
allocatedModeInfoMap = new Av1FrameModeInfoMap(
this.memoryAllocator,
new Size(
this.modeInfoSizePerSuperblock * this.superblockColumnCount,
this.modeInfoSizePerSuperblock * this.superblockRowCount));
// Tile parsing reconstructs each superblock before advancing to the next one. A single three-plane
// scratch owner therefore preserves every active transform while avoiding frame-wide retained copies.
int transformInfoScratchLength = checked(this.modeInfoCountPerSuperblock * 3);
allocatedTransformInfoScratch = this.memoryAllocator.Allocate<Av1TransformInfo>(transformInfoScratchLength, clean);
allocatedQuantizerIndices = this.memoryAllocator.Allocate2D<int>(1, superblockCount, clean);
// A 128x128 superblock contains four 64x64 CDEF filter blocks; a 64x64 superblock contains one.
this.cdefStrengthFactorLog2 = (superblockSizeLog2 - 6) << 1;
allocatedCdefStrength = this.memoryAllocator.Allocate2D<int>(1 << this.cdefStrengthFactorLog2, superblockCount, clean);
allocatedCdefStrength.MemoryGroup.Fill(-1);
allocatedDeltaLoopFilter = this.memoryAllocator.Allocate2D<int>(1 << this.deltaLoopFactorLog2, superblockCount, clean);
}
catch
{
allocatedDeltaLoopFilter?.Dispose();
allocatedCdefStrength?.Dispose();
allocatedQuantizerIndices?.Dispose();
allocatedTransformInfoScratch?.Dispose();
allocatedModeInfoMap?.Dispose();
allocatedModeInfoCounts?.Dispose();
allocatedModeInfos?.Dispose();
allocatedCoefficientScratch?.Dispose();
throw;
}
this.coefficientScratch = allocatedCoefficientScratch;
this.modeInfos = allocatedModeInfos;
this.modeInfoCounts = allocatedModeInfoCounts;
this.modeInfoMap = allocatedModeInfoMap;
this.transformInfoScratch = allocatedTransformInfoScratch;
this.quantizerIndices = allocatedQuantizerIndices;
this.cdefStrength = allocatedCdefStrength;
this.deltaLoopFilter = allocatedDeltaLoopFilter;
} }
/// <summary> /// <summary>
/// Gets the total mode-information capacity allocated for the frame. /// Gets the total mode-information capacity allocated for the frame.
/// </summary> /// </summary>
public int ModeInfoCount => this.modeInfos.Length; public int ModeInfoCount => checked((int)this.modeInfos.TotalLength);
/// <summary> /// <summary>
/// Gets the width or height of one square superblock in 4x4 mode-information units. /// Gets the width or height of one square superblock in 4x4 mode-information units.
/// </summary> /// </summary>
public int SuperblockModeInfoSize => this.modeInfoSizePerSuperblock; public int SuperblockModeInfoSize => this.modeInfoSizePerSuperblock;
/// <summary>
/// Gets a bit mask containing every luma transform type decoded in this frame.
/// </summary>
public int LumaTransformTypeCoverage { get; private set; }
/// <summary>
/// Gets the inter-prediction features selected by coding blocks in this frame.
/// </summary>
public Av1InterPredictionFeatures InterPredictionFeatures { get; private set; }
/// <summary>
/// Records one decoded luma transform type before the current-superblock scratch is reused.
/// </summary>
/// <param name="transformType">The decoded luma transform type.</param>
public void RecordLumaTransformType(Av1TransformType transformType) =>
this.LumaTransformTypeCoverage |= 1 << (int)transformType;
/// <summary>
/// Records the inter-prediction features selected by one completed coding block.
/// </summary>
/// <param name="modeInfo">The completed block mode information.</param>
/// <param name="frameHeader">The frame header containing global-motion parameters.</param>
public void RecordInterPredictionFeatures(Av1BlockModeInfo modeInfo, ObuFrameHeader frameHeader)
{
if (modeInfo.ReferenceFrames[0] <= Av1ReferenceFrameType.Intra)
{
return;
}
Av1InterPredictionFeatures features = Av1InterPredictionFeatures.None;
if (modeInfo.MotionMode == Av1MotionMode.Obmc)
{
features |= Av1InterPredictionFeatures.Obmc;
}
if (modeInfo.MotionMode == Av1MotionMode.Warped)
{
features |= Av1InterPredictionFeatures.LocalWarp;
}
if (modeInfo.YMode is Av1PredictionMode.GlobalMotionVector or Av1PredictionMode.GlobalGlobalMotionVector &&
Math.Min(modeInfo.BlockSize.GetWidth(), modeInfo.BlockSize.GetHeight()) >= 8)
{
int referenceCount = modeInfo.ReferenceFrames[1] > Av1ReferenceFrameType.Intra ? 2 : 1;
Span<Av1GlobalMotionParameters> globalMotionParameters = frameHeader.GetGlobalMotionParameters();
for (int referenceIndex = 0; referenceIndex < referenceCount; referenceIndex++)
{
int canonicalReferenceIndex =
(int)modeInfo.ReferenceFrames[referenceIndex] - (int)Av1ReferenceFrameType.Last;
Av1GlobalMotionParameters parameters = globalMotionParameters[canonicalReferenceIndex];
if (parameters.Type > Av1GlobalMotionType.Translation && !parameters.IsInvalid)
{
features |= Av1InterPredictionFeatures.GlobalWarp;
}
}
}
if (modeInfo.ReferenceFrames[1] == Av1ReferenceFrameType.Intra)
{
features |= modeInfo.UseInterIntraWedge
? Av1InterPredictionFeatures.WedgeInterIntra
: Av1InterPredictionFeatures.SmoothInterIntra;
}
else if (modeInfo.ReferenceFrames[1] > Av1ReferenceFrameType.Intra)
{
features |= modeInfo.CompoundType switch
{
Av1CompoundType.DistanceWeighted => Av1InterPredictionFeatures.DistanceWeightedCompound,
Av1CompoundType.Wedge => modeInfo.CompoundWedgeSign
? Av1InterPredictionFeatures.InvertedWedgeCompound
: Av1InterPredictionFeatures.WedgeCompound,
Av1CompoundType.DifferenceWeighted => modeInfo.DifferenceWeightedMaskType == Av1DifferenceWeightedMaskType.Type38Inverse
? Av1InterPredictionFeatures.InvertedDifferenceWeightedCompound
: Av1InterPredictionFeatures.DifferenceWeightedCompound,
_ => Av1InterPredictionFeatures.None,
};
}
this.InterPredictionFeatures |= features;
}
/// <summary> /// <summary>
/// Initializes the active frame's contiguous segment map and applies whole-map inheritance when requested. /// Initializes the active frame's contiguous segment map and applies whole-map inheritance when requested.
/// </summary> /// </summary>
/// <param name="frameHeader">The frame header defining active geometry and segmentation update behavior.</param> /// <param name="frameHeader">The frame header defining active geometry and segmentation update behavior.</param>
/// <param name="primaryReferenceFrameInfo"> /// <param name="primaryReferenceState">
/// The retained state selected by the primary reference, or <see langword="null"/> when no primary reference exists. /// The retained state selected by the primary reference, or <see langword="null"/> when no primary reference exists.
/// </param> /// </param>
public void InitializeSegmentIds(ObuFrameHeader frameHeader, Av1FrameInfo? primaryReferenceFrameInfo) public void InitializeSegmentIds(ObuFrameHeader frameHeader, ReferenceState? primaryReferenceState)
{ {
ObuSegmentationParameters segmentationParameters = frameHeader.SegmentationParameters; ObuSegmentationParameters segmentationParameters = frameHeader.SegmentationParameters;
if (!segmentationParameters.Enabled) if (!segmentationParameters.Enabled)
@ -223,16 +449,21 @@ internal partial class Av1FrameInfo : IDisposable
this.segmentIdColumnCount = frameHeader.ModeInfoColumnCount; this.segmentIdColumnCount = frameHeader.ModeInfoColumnCount;
this.segmentIdRowCount = frameHeader.ModeInfoRowCount; this.segmentIdRowCount = frameHeader.ModeInfoRowCount;
this.segmentIds = new byte[this.segmentIdColumnCount * this.segmentIdRowCount]; this.segmentIds = this.memoryAllocator.Allocate2D<byte>(
this.segmentIdColumnCount,
this.segmentIdRowCount,
AllocationOptions.Clean);
Buffer2D<byte>? primarySegmentIds = primaryReferenceState?.SegmentIds;
if (segmentationParameters.SegmentationUpdateMap == 0 && if (segmentationParameters.SegmentationUpdateMap == 0 &&
primaryReferenceFrameInfo is not null && primaryReferenceState is not null &&
primaryReferenceFrameInfo.segmentIdColumnCount == this.segmentIdColumnCount && primarySegmentIds is not null &&
primaryReferenceFrameInfo.segmentIdRowCount == this.segmentIdRowCount) primaryReferenceState.SegmentIdColumnCount == this.segmentIdColumnCount &&
primaryReferenceState.SegmentIdRowCount == this.segmentIdRowCount)
{ {
// AV1 decodemv.c copies the selected primary frame's block coverage when update_map is zero. Copying the // AV1 decodemv.c copies the selected primary frame's block coverage when update_map is zero. Copying the
// same contiguous map once establishes the identical final state without repeating a row copy per block. // same contiguous map once establishes the identical final state without repeating a row copy per block.
primaryReferenceFrameInfo.segmentIds.CopyTo(this.segmentIds, 0); primarySegmentIds.CopyTo(this.segmentIds);
} }
} }
@ -242,12 +473,19 @@ internal partial class Av1FrameInfo : IDisposable
/// <param name="row">The zero-based mode-information row.</param> /// <param name="row">The zero-based mode-information row.</param>
/// <param name="column">The zero-based mode-information column.</param> /// <param name="column">The zero-based mode-information column.</param>
/// <returns>The segment identifier stored at the requested position.</returns> /// <returns>The segment identifier stored at the requested position.</returns>
public byte GetSegmentId(int row, int column) => this.segmentIds[(row * this.segmentIdColumnCount) + column]; public byte GetSegmentId(int row, int column)
{
Buffer2D<byte> segmentIds = this.segmentIds
?? this.referenceState?.SegmentIds
?? throw new InvalidOperationException("The AV1 frame has no active segmentation map.");
return segmentIds[column, row];
}
/// <summary> /// <summary>
/// Gets the minimum retained segment identifier across a block's clipped mode-information coverage. /// Gets the minimum retained segment identifier across a block's clipped mode-information coverage.
/// </summary> /// </summary>
/// <param name="primaryReferenceFrameInfo"> /// <param name="primaryReferenceState">
/// The retained primary-frame state, or <see langword="null"/> when no compatible map is available. /// The retained primary-frame state, or <see langword="null"/> when no compatible map is available.
/// </param> /// </param>
/// <param name="blockSize">The block size whose 4x4 coverage is inspected.</param> /// <param name="blockSize">The block size whose 4x4 coverage is inspected.</param>
@ -255,12 +493,13 @@ internal partial class Av1FrameInfo : IDisposable
/// <returns> /// <returns>
/// The minimum retained segment identifier, or zero when no same-sized retained segmentation map is available. /// The minimum retained segment identifier, or zero when no same-sized retained segmentation map is available.
/// </returns> /// </returns>
public int GetPredictedSegmentId(Av1FrameInfo? primaryReferenceFrameInfo, Av1BlockSize blockSize, Point modeInfoPosition) public int GetPredictedSegmentId(ReferenceState? primaryReferenceState, Av1BlockSize blockSize, Point modeInfoPosition)
{ {
if (primaryReferenceFrameInfo is null || Buffer2D<byte>? primarySegmentIds = primaryReferenceState?.SegmentIds;
primaryReferenceFrameInfo.segmentIds.Length == 0 || if (primaryReferenceState is null ||
primaryReferenceFrameInfo.segmentIdColumnCount != this.segmentIdColumnCount || primarySegmentIds is null ||
primaryReferenceFrameInfo.segmentIdRowCount != this.segmentIdRowCount) primaryReferenceState.SegmentIdColumnCount != this.segmentIdColumnCount ||
primaryReferenceState.SegmentIdRowCount != this.segmentIdRowCount)
{ {
// the reference decoder exposes the prior map only when both mode-info dimensions match the active frame. Treating a // the reference decoder exposes the prior map only when both mode-info dimensions match the active frame. Treating a
// differently sized retained map as absent prevents coordinates from being reinterpreted with a new stride. // differently sized retained map as absent prevents coordinates from being reinterpreted with a new stride.
@ -275,8 +514,9 @@ internal partial class Av1FrameInfo : IDisposable
// dec_get_segment_id rule used when segmentation_temporal_update selects the retained primary map. // dec_get_segment_id rule used when segmentation_temporal_update selects the retained primary map.
for (int row = 0; row < rowCount; row++) for (int row = 0; row < rowCount; row++)
{ {
int offset = ((modeInfoPosition.Y + row) * primaryReferenceFrameInfo.segmentIdColumnCount) + modeInfoPosition.X; ReadOnlySpan<byte> segmentRow = primarySegmentIds
ReadOnlySpan<byte> segmentRow = primaryReferenceFrameInfo.segmentIds.AsSpan(offset, columnCount); .DangerousGetRowSpan(modeInfoPosition.Y + row)
.Slice(modeInfoPosition.X, columnCount);
for (int column = 0; column < segmentRow.Length; column++) for (int column = 0; column < segmentRow.Length; column++)
{ {
@ -297,13 +537,17 @@ internal partial class Av1FrameInfo : IDisposable
{ {
int columnCount = Math.Min(blockSize.Get4x4WideCount(), this.segmentIdColumnCount - modeInfoPosition.X); int columnCount = Math.Min(blockSize.Get4x4WideCount(), this.segmentIdColumnCount - modeInfoPosition.X);
int rowCount = Math.Min(blockSize.Get4x4HighCount(), this.segmentIdRowCount - modeInfoPosition.Y); int rowCount = Math.Min(blockSize.Get4x4HighCount(), this.segmentIdRowCount - modeInfoPosition.Y);
Buffer2D<byte> segmentIds = this.segmentIds
?? throw new InvalidOperationException("The AV1 frame has no writable segmentation map.");
// Each block contributes one ID to all covered 4x4 cells. Filling contiguous row slices retains the native // Each block contributes one ID to all covered 4x4 cells. Filling contiguous row slices retains the native
// row-major layout without the per-row object indirection of the previous jagged map. // row-major layout without the per-row object indirection of the previous jagged map.
for (int row = 0; row < rowCount; row++) for (int row = 0; row < rowCount; row++)
{ {
int offset = ((modeInfoPosition.Y + row) * this.segmentIdColumnCount) + modeInfoPosition.X; segmentIds
this.segmentIds.AsSpan(offset, columnCount).Fill((byte)segmentId); .DangerousGetRowSpan(modeInfoPosition.Y + row)
.Slice(modeInfoPosition.X, columnCount)
.Fill((byte)segmentId);
} }
} }
@ -333,15 +577,12 @@ internal partial class Av1FrameInfo : IDisposable
// unit count to nearest instead of unconditionally rounding a partial unit upward. // unit count to nearest instead of unconditionally rounding a partial unit upward.
int columnCount = Math.Max((planeWidth + (item.Size >> 1)) / item.Size, 1); int columnCount = Math.Max((planeWidth + (item.Size >> 1)) / item.Size, 1);
int rowCount = Math.Max((planeHeight + (item.Size >> 1)) / item.Size, 1); int rowCount = Math.Max((planeHeight + (item.Size >> 1)) / item.Size, 1);
Av1LoopRestorationUnit[] units = new Av1LoopRestorationUnit[columnCount * rowCount];
for (int i = 0; i < units.Length; i++)
{
units[i] = new();
}
this.loopRestorationUnitColumns[planeIndex] = columnCount; this.loopRestorationUnitColumns[planeIndex] = columnCount;
this.loopRestorationUnitRows[planeIndex] = rowCount; this.loopRestorationUnitRows[planeIndex] = rowCount;
this.loopRestorationUnits[planeIndex] = units; this.loopRestorationUnits[planeIndex] = this.memoryAllocator.Allocate2D<Av1LoopRestorationUnit>(
columnCount,
rowCount,
AllocationOptions.Clean);
} }
} }
@ -350,11 +591,17 @@ internal partial class Av1FrameInfo : IDisposable
/// </summary> /// </summary>
/// <param name="index">The position in the frame superblock grid.</param> /// <param name="index">The position in the frame superblock grid.</param>
/// <returns>The superblock view.</returns> /// <returns>The superblock view.</returns>
public Av1SuperblockInfo GetSuperblock(Point index) public Av1SuperblockInfo GetSuperblock(Point index) => new(this, index);
/// <summary>
/// Gets the number of mode-information records parsed for a specified superblock.
/// </summary>
/// <param name="index">The position in the frame superblock grid.</param>
/// <returns>The number of parsed records.</returns>
public int GetModeInfoCount(Point index)
{ {
Span<Av1SuperblockInfo> span = this.superblockInfos; int storageRow = (index.Y * this.superblockColumnCount) + index.X;
int i = (index.Y * this.superblockColumnCount) + index.X; return this.modeInfoCounts[0, storageRow];
return span[i];
} }
/// <summary> /// <summary>
@ -373,8 +620,7 @@ internal partial class Av1FrameInfo : IDisposable
public Av1BlockModeInfo GetModeInfo(Point superblockIndex, Point modeInfoIndex) public Av1BlockModeInfo GetModeInfo(Point superblockIndex, Point modeInfoIndex)
{ {
Point location = this.GetModeInfoPosition(superblockIndex, modeInfoIndex); Point location = this.GetModeInfoPosition(superblockIndex, modeInfoIndex);
int index = this.modeInfoMap[location]; return this.GetModeInfoByStorageIndex(this.modeInfoMap[location]);
return this.modeInfos[index];
} }
/// <summary> /// <summary>
@ -382,99 +628,74 @@ internal partial class Av1FrameInfo : IDisposable
/// </summary> /// </summary>
/// <param name="modeInfoPosition">The frame-relative position in 4x4 mode-information units.</param> /// <param name="modeInfoPosition">The frame-relative position in 4x4 mode-information units.</param>
/// <returns>The mode information covering the position.</returns> /// <returns>The mode information covering the position.</returns>
public Av1BlockModeInfo GetModeInfoAt(Point modeInfoPosition) => this.modeInfos[this.modeInfoMap[modeInfoPosition]]; public Av1BlockModeInfo GetModeInfoAt(Point modeInfoPosition)
=> this.GetModeInfoByStorageIndex(this.modeInfoMap[modeInfoPosition]);
/// <summary> /// <summary>
/// Gets the mode information records parsed for the specified superblock in bitstream order. /// Gets the mode information records parsed for the specified superblock in bitstream order.
/// </summary> /// </summary>
/// <param name="superblockIndex">The position in the frame superblock grid.</param> /// <param name="superblockIndex">The position in the frame superblock grid.</param>
/// <param name="count">The number of parsed records to return.</param> /// <param name="count">The number of parsed records to return.</param>
/// <returns>The parsed mode-information records.</returns> /// <returns>A discontiguous view of the parsed mode-information records.</returns>
public Span<Av1BlockModeInfo> GetModeInfos(Point superblockIndex, int count) public ModeInfoCollection GetModeInfos(Point superblockIndex, int count)
{ {
Point location = this.GetModeInfoPosition(superblockIndex, Point.Empty); int storageRow = (superblockIndex.Y * this.superblockColumnCount) + superblockIndex.X;
int index = this.modeInfoMap[location]; return new ModeInfoCollection(this, storageRow * this.modeInfoCountPerSuperblock, count);
return this.modeInfos.AsSpan(index, count);
} }
/// <summary> /// <summary>
/// Gets the transform-information storage for one plane of a specified superblock. /// Gets the transform-information scratch for one plane of the current superblock.
/// </summary> /// </summary>
/// <param name="plane">The zero-based plane index.</param> /// <param name="plane">The zero-based plane index.</param>
/// <param name="index">The position in the frame superblock grid.</param>
/// <returns>The luma storage for plane zero; otherwise, the shared chroma storage.</returns> /// <returns>The luma storage for plane zero; otherwise, the shared chroma storage.</returns>
public Span<Av1TransformInfo> GetSuperblockTransform(int plane, Point index) public Span<Av1TransformInfo> GetSuperblockTransform(int plane)
{ {
if (plane == 0) if (plane == 0)
{ {
return this.GetSuperblockTransformY(index); return this.GetSuperblockTransformY();
} }
return this.GetSuperblockTransformUv(index); return this.GetSuperblockTransformUv();
} }
/// <summary> /// <summary>
/// Gets the luma transform-information storage for a specified superblock. /// Gets the luma transform-information scratch for the current superblock.
/// </summary> /// </summary>
/// <param name="index">The position in the frame superblock grid.</param> /// <returns>The current-superblock luma transform-information span.</returns>
/// <returns>The superblock luma transform-information span.</returns> public Span<Av1TransformInfo> GetSuperblockTransformY()
public Span<Av1TransformInfo> GetSuperblockTransformY(Point index) => this.transformInfoScratch.GetSpan()[..this.modeInfoCountPerSuperblock];
{
Span<Av1TransformInfo> span = this.transformInfosY;
int offset = ((index.Y * this.superblockColumnCount) + index.X) * this.modeInfoCountPerSuperblock;
return span.Slice(offset, this.modeInfoCountPerSuperblock);
}
/// <summary> /// <summary>
/// Gets the shared chroma transform-information storage for a specified superblock. /// Gets the shared chroma transform-information scratch for the current superblock.
/// </summary> /// </summary>
/// <param name="index">The position in the frame superblock grid.</param> /// <returns>The current-superblock chroma transform-information span.</returns>
/// <returns>The superblock chroma transform-information span.</returns> public Span<Av1TransformInfo> GetSuperblockTransformUv()
public Span<Av1TransformInfo> GetSuperblockTransformUv(Point index) => this.transformInfoScratch.GetSpan().Slice(
{ this.modeInfoCountPerSuperblock,
Span<Av1TransformInfo> span = this.transformInfosUv; this.modeInfoCountPerSuperblock << 1);
int offset = (((index.Y * this.superblockColumnCount) + index.X) * this.modeInfoCountPerSuperblock) << 1;
return span.Slice(offset, this.modeInfoCountPerSuperblock << 1);
}
/// <summary> /// <summary>
/// Gets the luma coefficient storage for a specified superblock. /// Gets the luma coefficient scratch reused for the current superblock.
/// </summary> /// </summary>
/// <param name="index">The position in the frame superblock grid.</param> /// <returns>The current superblock luma coefficient span.</returns>
/// <returns>The superblock luma coefficient span.</returns> public Span<int> GetCoefficientsY()
public Span<int> GetCoefficientsY(Point index) => this.coefficientScratch.GetSpan()[..this.lumaCoefficientCount];
{
Span<int> span = this.coefficientsY;
int count = this.modeInfoCountPerSuperblock * CoefficientCountPerModeInfo;
int superblock = (index.Y * this.superblockColumnCount) + index.X;
return span.Slice(superblock * count, count);
}
/// <summary> /// <summary>
/// Gets the blue-difference chroma coefficient storage for a specified superblock. /// Gets the blue-difference chroma coefficient scratch reused for the current superblock.
/// </summary> /// </summary>
/// <param name="index">The position in the frame superblock grid.</param> /// <returns>The current superblock blue-difference chroma coefficient span.</returns>
/// <returns>The superblock blue-difference chroma coefficient span.</returns> public Span<int> GetCoefficientsU()
public Span<int> GetCoefficientsU(Point index) => this.coefficientScratch.GetSpan().Slice(this.lumaCoefficientCount, this.chromaCoefficientCount);
{
Span<int> span = this.coefficientsU;
int count = (this.modeInfoCountPerSuperblock * CoefficientCountPerModeInfo) >> this.subsamplingFactor;
int superblock = (index.Y * this.superblockColumnCount) + index.X;
return span.Slice(superblock * count, count);
}
/// <summary> /// <summary>
/// Gets the red-difference chroma coefficient storage for a specified superblock. /// Gets the red-difference chroma coefficient scratch reused for the current superblock.
/// </summary> /// </summary>
/// <param name="index">The position in the frame superblock grid.</param> /// <returns>The current superblock red-difference chroma coefficient span.</returns>
/// <returns>The superblock red-difference chroma coefficient span.</returns> public Span<int> GetCoefficientsV()
public Span<int> GetCoefficientsV(Point index) => this.coefficientScratch.GetSpan().Slice(
{ this.lumaCoefficientCount + this.chromaCoefficientCount,
Span<int> span = this.coefficientsV; this.chromaCoefficientCount);
int count = (this.modeInfoCountPerSuperblock * CoefficientCountPerModeInfo) >> this.subsamplingFactor;
int superblock = (index.Y * this.superblockColumnCount) + index.X;
return span.Slice(superblock * count, count);
}
/// <summary> /// <summary>
/// Gets a reference to the active base quantizer index for a specified superblock. /// Gets a reference to the active base quantizer index for a specified superblock.
@ -483,9 +704,8 @@ internal partial class Av1FrameInfo : IDisposable
/// <returns>A reference to the superblock base quantizer index.</returns> /// <returns>A reference to the superblock base quantizer index.</returns>
public ref int GetQuantizerIndex(Point index) public ref int GetQuantizerIndex(Point index)
{ {
Span<int> span = this.quantizerIndices; int storageRow = (index.Y * this.superblockColumnCount) + index.X;
int i = (index.Y * this.superblockColumnCount) + index.X; return ref this.quantizerIndices[0, storageRow];
return ref span[i];
} }
/// <summary> /// <summary>
@ -495,9 +715,8 @@ internal partial class Av1FrameInfo : IDisposable
/// <returns>The superblock filter-strength span.</returns> /// <returns>The superblock filter-strength span.</returns>
public Span<int> GetCdefStrength(Point index) public Span<int> GetCdefStrength(Point index)
{ {
Span<int> span = this.cdefStrength; int storageRow = (index.Y * this.superblockColumnCount) + index.X;
int i = ((index.Y * this.superblockColumnCount) + index.X) << this.cdefStrengthFactorLog2; return this.cdefStrength.DangerousGetRowSpan(storageRow);
return span.Slice(i, 1 << this.cdefStrengthFactorLog2);
} }
/// <summary> /// <summary>
@ -520,9 +739,8 @@ internal partial class Av1FrameInfo : IDisposable
/// <returns>The superblock loop-filter delta span.</returns> /// <returns>The superblock loop-filter delta span.</returns>
public Span<int> GetDeltaLoopFilter(Point index) public Span<int> GetDeltaLoopFilter(Point index)
{ {
Span<int> span = this.deltaLoopFilter; int storageRow = (index.Y * this.superblockColumnCount) + index.X;
int i = ((index.Y * this.superblockColumnCount) + index.X) << this.deltaLoopFactorLog2; return this.deltaLoopFilter.DangerousGetRowSpan(storageRow);
return span.Slice(i, 1 << this.deltaLoopFactorLog2);
} }
/// <summary> /// <summary>
@ -546,10 +764,12 @@ internal partial class Av1FrameInfo : IDisposable
/// <param name="row">The restoration-unit row.</param> /// <param name="row">The restoration-unit row.</param>
/// <param name="column">The restoration-unit column.</param> /// <param name="column">The restoration-unit column.</param>
/// <returns>The decoded restoration-unit information.</returns> /// <returns>The decoded restoration-unit information.</returns>
public Av1LoopRestorationUnit GetLoopRestorationUnit(int plane, int row, int column) public ref Av1LoopRestorationUnit GetLoopRestorationUnit(int plane, int row, int column)
{ {
int index = (row * this.loopRestorationUnitColumns[plane]) + column; Buffer2D<Av1LoopRestorationUnit> units = this.loopRestorationUnits[plane]
return this.loopRestorationUnits[plane][index]; ?? throw new InvalidOperationException("The selected AV1 plane has no loop-restoration units.");
return ref units[column, row];
} }
/// <summary> /// <summary>
@ -560,10 +780,27 @@ internal partial class Av1FrameInfo : IDisposable
public void UpdateModeInfo(Av1BlockModeInfo modeInfo, Av1SuperblockInfo superblockInfo) public void UpdateModeInfo(Av1BlockModeInfo modeInfo, Av1SuperblockInfo superblockInfo)
{ {
Point modeInfoPosition = this.GetModeInfoPosition(superblockInfo.Position, modeInfo.PositionInSuperblock); Point modeInfoPosition = this.GetModeInfoPosition(superblockInfo.Position, modeInfo.PositionInSuperblock);
int storageRow = (superblockInfo.Position.Y * this.superblockColumnCount) + superblockInfo.Position.X;
ref int modeInfoCount = ref this.modeInfoCounts[0, storageRow];
DebugGuard.MustBeLessThan(modeInfoCount, this.modeInfoCountPerSuperblock, nameof(modeInfoCount));
int storageIndex = (storageRow * this.modeInfoCountPerSuperblock) + modeInfoCount;
modeInfo.ModeInfoIndex = this.modeInfoMap.NextIndex; modeInfo.ModeInfoIndex = this.modeInfoMap.NextIndex;
this.modeInfos[this.modeInfoMap.NextIndex] = modeInfo; this.GetModeInfoByStorageIndex(storageIndex) = modeInfo;
modeInfoCount++;
this.UpdateRetainedMotionField(modeInfo, modeInfoPosition); this.UpdateRetainedMotionField(modeInfo, modeInfoPosition);
this.modeInfoMap.Update(modeInfoPosition, modeInfo.BlockSize); this.modeInfoMap.Update(modeInfoPosition, modeInfo.BlockSize, storageIndex);
}
/// <summary>
/// Gets a reference to one packed mode-information record across allocator segments.
/// </summary>
/// <param name="storageIndex">The packed frame-storage index.</param>
/// <returns>A reference to the selected mode information.</returns>
private ref Av1BlockModeInfo GetModeInfoByStorageIndex(int storageIndex)
{
int bufferIndex = storageIndex / this.modeInfos.BufferLength;
int elementIndex = storageIndex - (bufferIndex * this.modeInfos.BufferLength);
return ref this.modeInfos[bufferIndex].Span[elementIndex];
} }
/// <summary> /// <summary>
@ -578,4 +815,85 @@ internal partial class Av1FrameInfo : IDisposable
int y = (superblockPosition.Y * this.modeInfoSizePerSuperblock) + positionInSuperblock.Y; int y = (superblockPosition.Y * this.modeInfoSizePerSuperblock) + positionInSuperblock.Y;
return new Point(x, y); return new Point(x, y);
} }
/// <summary>
/// Provides indexed and reference-preserving traversal over one superblock's discontiguous mode information.
/// </summary>
public readonly struct ModeInfoCollection
{
private readonly Av1FrameInfo owner;
private readonly int startIndex;
/// <summary>
/// Initializes a new instance of the <see cref="ModeInfoCollection"/> struct.
/// </summary>
/// <param name="owner">The frame that owns the mode-information group.</param>
/// <param name="startIndex">The packed index of the first record.</param>
/// <param name="length">The number of records in the view.</param>
public ModeInfoCollection(Av1FrameInfo owner, int startIndex, int length)
{
this.owner = owner;
this.startIndex = startIndex;
this.Length = length;
}
/// <summary>
/// Gets the number of records in the view.
/// </summary>
public int Length { get; }
/// <summary>
/// Gets a reference to the record at the specified traversal index.
/// </summary>
/// <param name="index">The zero-based traversal index.</param>
public ref Av1BlockModeInfo this[int index] =>
ref this.owner.GetModeInfoByStorageIndex(this.startIndex + index);
/// <summary>
/// Creates a reference-preserving enumerator over the records.
/// </summary>
/// <returns>The initialized enumerator.</returns>
public Enumerator GetEnumerator() => new(this.owner, this.startIndex, this.Length);
/// <summary>
/// Enumerates one superblock's mode-information records without flattening allocator segments.
/// </summary>
public struct Enumerator
{
private readonly Av1FrameInfo owner;
private readonly int startIndex;
private readonly int length;
private int index;
/// <summary>
/// Initializes a new instance of the <see cref="Enumerator"/> struct.
/// </summary>
/// <param name="owner">The frame that owns the mode-information group.</param>
/// <param name="startIndex">The packed index of the first record.</param>
/// <param name="length">The number of records in the view.</param>
public Enumerator(Av1FrameInfo owner, int startIndex, int length)
{
this.owner = owner;
this.startIndex = startIndex;
this.length = length;
this.index = -1;
}
/// <summary>
/// Gets a reference to the current record.
/// </summary>
public ref Av1BlockModeInfo Current =>
ref this.owner.GetModeInfoByStorageIndex(this.startIndex + this.index);
/// <summary>
/// Advances to the next record.
/// </summary>
/// <returns><see langword="true"/> when another record is available.</returns>
public bool MoveNext()
{
this.index++;
return this.index < this.length;
}
}
}
} }

50
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameModeInfoMap.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// <summary> /// <summary>
@ -11,12 +13,12 @@ internal partial class Av1FrameInfo
/// <summary> /// <summary>
/// Mapping of <see cref="Av1BlockModeInfo"/> values, from position to index into the <see cref="Av1FrameInfo"/>. /// Mapping of <see cref="Av1BlockModeInfo"/> values, from position to index into the <see cref="Av1FrameInfo"/>.
/// </summary> /// </summary>
public class Av1FrameModeInfoMap public sealed class Av1FrameModeInfoMap : IDisposable
{ {
/// <summary> /// <summary>
/// Stores the mode-information index assigned to each aligned 4x4 frame location. /// Stores the mode-information index assigned to each aligned 4x4 frame location.
/// </summary> /// </summary>
private readonly int[] offsets; private readonly MemoryGroup<int> offsets;
/// <summary> /// <summary>
/// The dimensions of <see cref="offsets"/> in 4x4 mode-information units. /// The dimensions of <see cref="offsets"/> in 4x4 mode-information units.
@ -24,14 +26,25 @@ internal partial class Av1FrameInfo
private readonly Size alignedModeInfoCount; private readonly Size alignedModeInfoCount;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1FrameModeInfoMap"/> class. /// Initializes a new instance of the <see cref="Av1FrameModeInfoMap"/> class using the default allocator.
/// </summary> /// </summary>
/// <param name="modeInfoCount">The aligned frame dimensions in 4x4 mode-information units.</param> /// <param name="modeInfoCount">The aligned frame dimensions in 4x4 mode-information units.</param>
public Av1FrameModeInfoMap(Size modeInfoCount) public Av1FrameModeInfoMap(Size modeInfoCount)
: this(Configuration.Default.MemoryAllocator, modeInfoCount)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Av1FrameModeInfoMap"/> class.
/// </summary>
/// <param name="memoryAllocator">The allocator providing frame-sized storage.</param>
/// <param name="modeInfoCount">The aligned frame dimensions in 4x4 mode-information units.</param>
public Av1FrameModeInfoMap(MemoryAllocator memoryAllocator, Size modeInfoCount)
{ {
this.alignedModeInfoCount = modeInfoCount; this.alignedModeInfoCount = modeInfoCount;
this.NextIndex = 0; this.NextIndex = 0;
this.offsets = new int[this.alignedModeInfoCount.Width * this.alignedModeInfoCount.Height]; long offsetCount = (long)this.alignedModeInfoCount.Width * this.alignedModeInfoCount.Height;
this.offsets = memoryAllocator.AllocateGroup<int>(offsetCount, 1, AllocationOptions.Clean);
} }
/// <summary> /// <summary>
@ -47,8 +60,8 @@ internal partial class Av1FrameInfo
{ {
get get
{ {
int index = (location.Y * this.alignedModeInfoCount.Width) + location.X; long offset = ((long)location.Y * this.alignedModeInfoCount.Width) + location.X;
return this.offsets[index]; return this.offsets.GetRemainingSliceOfBuffer(offset)[0];
} }
} }
@ -57,7 +70,8 @@ internal partial class Av1FrameInfo
/// </summary> /// </summary>
/// <param name="modeInfoLocation">The block origin in 4x4 mode-information units.</param> /// <param name="modeInfoLocation">The block origin in 4x4 mode-information units.</param>
/// <param name="blockSize">The decoded block size.</param> /// <param name="blockSize">The decoded block size.</param>
public void Update(Point modeInfoLocation, Av1BlockSize blockSize) /// <param name="storageIndex">The packed frame-storage index assigned to the decoded block.</param>
public void Update(Point modeInfoLocation, Av1BlockSize blockSize, int storageIndex)
{ {
int bw4 = blockSize.Get4x4WideCount(); int bw4 = blockSize.Get4x4WideCount();
int bh4 = blockSize.Get4x4HighCount(); int bh4 = blockSize.Get4x4HighCount();
@ -70,10 +84,30 @@ internal partial class Av1FrameInfo
// because later blocks query their above and left neighbors at cell granularity. // because later blocks query their above and left neighbors at cell granularity.
for (int i = modeInfoLocation.Y; i < modeInfoLocation.Y + bh4; i++) for (int i = modeInfoLocation.Y; i < modeInfoLocation.Y + bh4; i++)
{ {
Array.Fill(this.offsets, this.NextIndex, (i * this.alignedModeInfoCount.Width) + modeInfoLocation.X, bw4); long offset = ((long)i * this.alignedModeInfoCount.Width) + modeInfoLocation.X;
int remaining = bw4;
while (remaining > 0)
{
Span<int> destination = this.offsets.GetRemainingSliceOfBuffer(offset);
int count = Math.Min(remaining, destination.Length);
destination[..count].Fill(storageIndex);
offset += count;
remaining -= count;
}
} }
this.NextIndex++; this.NextIndex++;
} }
/// <summary>
/// Maps every 4x4 location covered by a decoded block to its traversal-order index.
/// </summary>
/// <param name="modeInfoLocation">The block origin in 4x4 mode-information units.</param>
/// <param name="blockSize">The decoded block size.</param>
public void Update(Point modeInfoLocation, Av1BlockSize blockSize)
=> this.Update(modeInfoLocation, blockSize, this.NextIndex);
/// <inheritdoc/>
public void Dispose() => this.offsets.Dispose();
} }
} }

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

Loading…
Cancel
Save