From 7d8af2b96825b25d046003c6cbd8f9179955d620 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 1 Sep 2026 15:49:00 +1000 Subject: [PATCH] Complete AV1 decoder audit remediation --- HEIF_IMPLEMENTATION_PLAN.md | 37 +- .../Formats/Heif/Av1/Av1CodecConfiguration.cs | 2 +- src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs | 331 +++++--- .../Formats/Heif/Av1/Av1FrameBuffer.cs | 265 +++++-- .../Heif/Av1/Color/Av1PlanarSampleBuffer.cs | 2 +- .../Av1/Color/Av1PresentationSampleBuffer.cs | 91 ++- .../Heif/Av1/Entropy/Av1Distribution.cs | 4 +- .../Av1/Entropy/Av1FrameEntropyContexts.cs | 41 +- .../Heif/Av1/Entropy/Av1SymbolDecoder.cs | 13 - .../Av1/OpenBitstreamUnit/ObuColorConfig.cs | 2 +- ...tDirectionalEnhancementFilterParameters.cs | 20 +- .../OpenBitstreamUnit/ObuDecoderModelInfo.cs | 2 +- .../OpenBitstreamUnit/ObuDeltaParameters.cs | 2 +- .../ObuFilmGrainParameters.cs | 2 +- .../Av1/OpenBitstreamUnit/ObuFrameHeader.cs | 11 +- .../Av1/OpenBitstreamUnit/ObuFrameSize.cs | 2 +- .../Heif/Av1/OpenBitstreamUnit/ObuHeader.cs | 2 +- .../ObuLoopFilterParameters.cs | 31 +- .../ObuLoopRestorationItem.cs | 4 +- .../ObuLoopRestorationParameters.cs | 16 +- .../OpenBitstreamUnit/ObuOperatingPoint.cs | 2 +- .../Av1/OpenBitstreamUnit/ObuOrderHintInfo.cs | 2 +- .../ObuQuantizationParameters.cs | 38 +- .../Heif/Av1/OpenBitstreamUnit/ObuReader.cs | 481 ++++++++++-- .../ObuSegmentationParameters.cs | 55 +- .../OpenBitstreamUnit/ObuSequenceHeader.cs | 15 +- .../ObuSkipModeParameters.cs | 2 +- .../OpenBitstreamUnit/ObuTileGroupHeader.cs | 27 +- .../Av1/OpenBitstreamUnit/ObuTimingInfo.cs | 2 +- .../Heif/Av1/OpenBitstreamUnit/ObuWriter.cs | 4 +- .../Heif/Av1/Pipeline/Av1FrameDecoder.cs | 42 +- .../Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs | 705 ++++++++++++------ .../Pipeline/FilmGrain/Av1FilmGrainDecoder.cs | 12 +- .../LoopFilter/Av1LoopFilterContext.cs | 143 +++- .../LoopFilter/Av1LoopFilterDecoder.cs | 4 +- .../Av1LoopRestorationBoundary.cs | 17 +- .../Av1LoopRestorationDecoder.cs | 332 ++++++--- .../Quantizers/Av1DeQuantizationContext.cs | 10 +- .../Av1InverseQuantizationLookup.cs | 2 +- .../Quantizers/Av1InverseQuantizer.cs | 4 +- .../Quantizers/Av1QuantizationLookup.cs | 7 +- .../Av1SuperResolutionDecoder.cs | 2 +- .../Av1BottomRightTopLeftConstants.cs | 136 +--- ...Av1DirectionalIntraPredictor.Operations.cs | 8 +- ...ionalIntraPredictor.HorizontalOperator.cs} | 2 +- ...1NonDirectionalIntraPredictor.Operator.cs} | 4 +- ...v1NonDirectionalIntraPredictor.Packing.cs} | 2 +- ...irectionalIntraPredictor.PaethOperator.cs} | 2 +- ...ntraPredictor.SmoothHorizontalOperator.cs} | 2 +- ...rectionalIntraPredictor.SmoothOperator.cs} | 2 +- ...lIntraPredictor.SmoothVerticalOperator.cs} | 2 +- ...ctionalIntraPredictor.VerticalOperator.cs} | 2 +- ...=> Av1NonDirectionalIntraPredictorBase.cs} | 16 +- .../Av1/Prediction/Av1PredictionDecoder.cs | 32 +- .../Av1ChromaFromLumaContext.Operations.cs | 6 +- .../Av1ChromaFromLumaContext.cs | 33 +- .../Av1CompoundAveragePredictor.Operator.cs | 2 +- .../Inter/Av1CompoundAveragePredictor.cs | 2 +- ...poundDistanceWeightedPredictor.Operator.cs | 38 +- .../Av1CompoundDistanceWeightedPredictor.cs | 2 +- .../Av1CompoundInterPredictor.Operator.cs | 32 +- .../Inter/Av1CompoundInterPredictor.cs | 2 +- ...ndIntermediateAveragePredictor.Operator.cs | 2 +- ...Av1CompoundIntermediateAveragePredictor.cs | 2 +- ...eDifferenceWeightedMaskBuilder.Operator.cs | 2 +- ...termediateDifferenceWeightedMaskBuilder.cs | 2 +- ...diateDistanceWeightedPredictor.Operator.cs | 2 +- ...ndIntermediateDistanceWeightedPredictor.cs | 2 +- ...IntermediateMaskBlendPredictor.Operator.cs | 2 +- ...1CompoundIntermediateMaskBlendPredictor.cs | 2 +- .../Av1CompoundMaskBlendPredictor.Operator.cs | 50 +- .../Inter/Av1CompoundMaskBlendPredictor.cs | 2 +- ...1DifferenceWeightedMaskBuilder.Operator.cs | 2 +- .../Inter/Av1DifferenceWeightedMaskBuilder.cs | 2 +- .../Inter/Av1InterIntraMaskBuilder.cs | 2 +- .../Inter/Av1ScaledInterPredictor.Operator.cs | 14 +- .../Inter/Av1ScaledInterPredictor.cs | 8 +- ...TranslationalInterPredictor.Arithmetic.cs} | 26 +- ...ationalInterPredictor.BilinearOperator.cs} | 2 +- ...v1TranslationalInterPredictor.Dispatch.cs} | 2 +- ...Av1TranslationalInterPredictor.Filters.cs} | 2 +- ...anslationalInterPredictor.OneDimension.cs} | 2 +- ...v1TranslationalInterPredictor.Operator.cs} | 2 +- ...lationalInterPredictor.RegularOperator.cs} | 2 +- ...nslationalInterPredictor.SharpOperator.cs} | 2 +- ...slationalInterPredictor.SmoothOperator.cs} | 2 +- ...ionalInterPredictor.TwoDimensions.Byte.cs} | 18 +- ...nalInterPredictor.TwoDimensions.UInt16.cs} | 10 +- ...r.cs => Av1TranslationalInterPredictor.cs} | 2 +- .../Inter/Av1WarpedInterPredictor.Filters.cs | 2 +- .../Inter/Av1WarpedInterPredictor.Operator.cs | 2 +- .../Inter/Av1WarpedInterPredictor.cs | 2 +- src/ImageSharp/Formats/Heif/Av1/Readme.md | 32 +- .../Av1/ReferenceFrames/Av1ReferenceFrame.cs | 145 +++- .../Av1ReferenceFrameBorder.cs | 6 +- .../ReferenceFrames/Av1ReferenceFrameStore.cs | 25 +- .../Heif/Av1/Tiling/Av1BlockModeInfo.cs | 19 +- .../Av1/Tiling/Av1FrameInfo.MotionField.cs | 360 +++++++-- .../Formats/Heif/Av1/Tiling/Av1FrameInfo.cs | 656 +++++++++++----- .../Heif/Av1/Tiling/Av1FrameModeInfoMap.cs | 50 +- .../Heif/Av1/Tiling/Av1LoopRestorationUnit.cs | 42 +- .../Heif/Av1/Tiling/Av1NeighborArrayUnit.cs | 10 - .../Heif/Av1/Tiling/Av1PartitionContext.cs | 8 +- .../Heif/Av1/Tiling/Av1SuperblockInfo.cs | 49 +- .../Formats/Heif/Av1/Tiling/Av1TileInfo.cs | 2 +- .../Formats/Heif/Av1/Tiling/Av1TileReader.cs | 178 +++-- .../Formats/Heif/Av1/Tiling/Av1TileWriter.cs | 18 +- .../Av1/Tiling/Av1TransformBlockContext.cs | 2 +- .../Heif/Av1/Transform/Av1BlockDecoder.cs | 184 ++--- ...v1InverseTransformer.ByteOutputOperator.cs | 2 +- ...eTransformer.HighBitDepthOutputOperator.cs | 2 +- .../Av1InverseTransformer.Operator.cs | 2 +- .../Av1/Transform/Av1InverseTransformer.cs | 2 +- .../Formats/Heif/Av1HeifItemDecoder.cs | 2 +- .../Formats/Heif/GridHeifItemDecoder.cs | 276 ++++--- .../Formats/Heif/HeifDecoderCore.cs | 270 ++++--- src/ImageSharp/Formats/Heif/HeifItem.cs | 2 +- src/ImageSharp/Formats/Heif/HeifItemLink.cs | 2 +- src/ImageSharp/Formats/Heif/HeifLocation.cs | 2 +- .../Formats/Heif/HeifLocationComparer.cs | 2 +- .../Heif/Av1ColorConversionBenchmarks.cs | 26 +- .../Formats/Heif/Av1/Av1BitStreamTests.cs | 4 - .../Heif/Av1/Av1ChromaFromLumaTests.cs | 10 +- .../Heif/Av1/Av1CompoundBlockDecoderTests.cs | 43 +- .../Heif/Av1/Av1DeblockingFilterTests.cs | 5 +- .../Heif/Av1/Av1ForwardTransformTests.cs | 8 +- .../Formats/Heif/Av1/Av1FrameBufferTests.cs | 183 ++++- .../Heif/Av1/Av1InterFrameModeInfoTests.cs | 12 +- .../Heif/Av1/Av1IntraBlockCopyTests.cs | 24 +- .../Heif/Av1/Av1MotionModeInfoTests.cs | 24 +- .../Av1/Av1MotionVariationCandidatesTests.cs | 34 +- .../Formats/Heif/Av1/Av1PredictorTests.cs | 45 +- .../Av1/Av1ReconstructionConformanceTests.cs | 284 ++----- .../Heif/Av1/Av1ReferenceFrameStoreTests.cs | 137 ++-- .../Av1/Av1ReferenceMotionVectorsTests.cs | 26 +- .../Heif/Av1/Av1ReferenceYuvConverter.cs | 7 +- .../Heif/Av1/Av1TemporalSegmentationTests.cs | 23 +- .../Formats/Heif/Av1/Av1TilingTests.cs | 131 ++-- ...=> Av1TranslationalInterPredictorTests.cs} | 94 ++- .../Formats/Heif/Av1/Av1YuvConverterTests.cs | 8 +- .../Formats/Heif/Av1/ObuFrameHeaderTests.cs | 46 +- .../Heif/Av1/ObuFrameLifecycleTests.cs | 29 +- .../Formats/Heif/HeifDecoderTests.cs | 11 +- .../Formats/Heif/HeifSequenceParserTests.cs | 13 +- .../TestUtilities/TestMemoryAllocator.cs | 3 +- 145 files changed, 4587 insertions(+), 2282 deletions(-) rename src/ImageSharp/Formats/Heif/Av1/Prediction/{Av1IntraPredictor.HorizontalOperator.cs => Av1NonDirectionalIntraPredictor.HorizontalOperator.cs} (97%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/{Av1IntraPredictor.Operator.cs => Av1NonDirectionalIntraPredictor.Operator.cs} (99%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/{Av1IntraPredictor.Packing.cs => Av1NonDirectionalIntraPredictor.Packing.cs} (99%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/{Av1IntraPredictor.PaethOperator.cs => Av1NonDirectionalIntraPredictor.PaethOperator.cs} (99%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/{Av1IntraPredictor.SmoothHorizontalOperator.cs => Av1NonDirectionalIntraPredictor.SmoothHorizontalOperator.cs} (99%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/{Av1IntraPredictor.SmoothOperator.cs => Av1NonDirectionalIntraPredictor.SmoothOperator.cs} (99%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/{Av1IntraPredictor.SmoothVerticalOperator.cs => Av1NonDirectionalIntraPredictor.SmoothVerticalOperator.cs} (99%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/{Av1IntraPredictor.VerticalOperator.cs => Av1NonDirectionalIntraPredictor.VerticalOperator.cs} (97%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/{Av1IntraPredictorBase.cs => Av1NonDirectionalIntraPredictorBase.cs} (87%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/{Av1InterPredictor.Arithmetic.cs => Av1TranslationalInterPredictor.Arithmetic.cs} (89%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/{Av1InterPredictor.BilinearOperator.cs => Av1TranslationalInterPredictor.BilinearOperator.cs} (90%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/{Av1InterPredictor.Dispatch.cs => Av1TranslationalInterPredictor.Dispatch.cs} (99%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/{Av1InterPredictor.Filters.cs => Av1TranslationalInterPredictor.Filters.cs} (98%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/{Av1InterPredictor.OneDimension.cs => Av1TranslationalInterPredictor.OneDimension.cs} (99%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/{Av1InterPredictor.Operator.cs => Av1TranslationalInterPredictor.Operator.cs} (99%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/{Av1InterPredictor.RegularOperator.cs => Av1TranslationalInterPredictor.RegularOperator.cs} (91%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/{Av1InterPredictor.SharpOperator.cs => Av1TranslationalInterPredictor.SharpOperator.cs} (93%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/{Av1InterPredictor.SmoothOperator.cs => Av1TranslationalInterPredictor.SmoothOperator.cs} (91%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/{Av1InterPredictor.TwoDimensions.Byte.cs => Av1TranslationalInterPredictor.TwoDimensions.Byte.cs} (95%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/{Av1InterPredictor.TwoDimensions.UInt16.cs => Av1TranslationalInterPredictor.TwoDimensions.UInt16.cs} (98%) rename src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/{Av1InterPredictor.cs => Av1TranslationalInterPredictor.cs} (99%) rename tests/ImageSharp.Tests/Formats/Heif/Av1/{Av1InterPredictorTests.cs => Av1TranslationalInterPredictorTests.cs} (94%) diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index f515376fa..7e08642f2 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/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 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. -- [ ] 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: @@ -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. - [x] The established paused-stream cancellation suite now includes AVIF. It verifies cancellation at 0%, 30%, and 70% of both file and memory streams, plus pre-cancelled identification, on both targets. +- [x] The completed checkpoint was committed as + `7f0e08126b3354e8f1eb45886f0d572006ae27de` with author and committer + `James Jackson-South `. 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` 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: - [ ] Every supported native format and AV1 tool has exact current-main libaom production-path evidence. diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs b/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs index 8b5d63cb6..87c2660d9 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs @@ -551,7 +551,7 @@ internal sealed class Av1CodecConfiguration /// The source description used by invalid-content errors. /// Receives decoded content light-level metadata when present. /// Receives decoded mastering-display metadata when present. - private static void ReadHdrMetadata( + public static void ReadHdrMetadata( ReadOnlySpan payload, string sourceName, out HeifContentLightLevel? contentLightLevel, diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs index 01fc9ba2b..a926fec5e 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs @@ -73,19 +73,9 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable private ObuSequenceHeader? validatedSequenceHeader; /// - /// 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. /// - private Av1TileReader? tileReader; - - /// - /// The destination sample buffer for the frame currently being parsed and reconstructed. - /// - private Av1FrameBuffer? frameBuffer; - - /// - /// The reconstruction pipeline for the frame currently being parsed. - /// - private Av1FrameDecoder? frameDecoder; + private FrameDecodeState? frameDecodeState; /// /// Initializes a new instance of the class. @@ -139,10 +129,16 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable public ObuSequenceHeader? SequenceHeader { get; private set; } /// - /// Gets the tile and superblock state for the final retained shown frame, or before one completes. + /// Gets tile and superblock state for the most recently reconstructed frame, or when no + /// frame was reconstructed or the output selected an existing reference without new tile syntax. /// public Av1FrameInfo? FrameInfo { get; private set; } + /// + /// Gets the inter-prediction features selected by every coded frame completed in the most recently decoded payload. + /// + public Av1InterPredictionFeatures DecodedInterPredictionFeatures { get; private set; } + /// /// Gets the native planes of the current retained shown frame, or before one completes. /// @@ -183,6 +179,15 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable CicpProfile = effectiveColorProfile }; + HeifContentLightLevel? contentLightLevel = this.obuReader.ContentLightLevel; + HeifMasteringDisplayColorVolume? masteringDisplayColorVolume = this.obuReader.MasteringDisplayColorVolume; + if (contentLightLevel is not null || masteringDisplayColorVolume is not null) + { + HeifMetadata heifMetadata = metadata.GetHeifMetadata(); + heifMetadata.ContentLightLevel = contentLightLevel; + heifMetadata.MasteringDisplayColorVolume = masteringDisplayColorVolume; + } + try { return new Image(this.configuration, metadata, [frame]); @@ -224,9 +229,10 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable containerColorProfile, codecConfiguration, out effectiveColorProfile, + out ObuFrameHeader frameHeader, layeredImageIndex); - return this.ConvertToFrame(frameBuffer, effectiveColorProfile, presentationSize); + return this.ConvertToFrame(frameBuffer, frameHeader, effectiveColorProfile, presentationSize); } /// @@ -250,7 +256,8 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable null, requireShownFrame: true); - return this.ConvertToFrame(this.referenceFrames.OutputFrame!.FrameBuffer, effectiveColorProfile); + Av1ReferenceFrame outputFrame = this.referenceFrames.ResolveOutput(); + return this.ConvertToFrame(outputFrame.FrameBuffer, outputFrame.FrameHeader, effectiveColorProfile); } /// @@ -300,8 +307,9 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable null, requireShownFrame: true); + Av1ReferenceFrame outputFrame = this.referenceFrames.ResolveOutput(); this.ComposeAlpha( - this.referenceFrames.OutputFrame!.FrameBuffer, + outputFrame.FrameBuffer, expectedCodedSize, destination, outputSize, @@ -314,11 +322,13 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable /// /// The destination pixel type. /// The decoded native planes. + /// The completed header describing the decoded native planes. /// The effective CICP description. /// The requested item presentation size, or an empty size for the coded dimensions. /// The independently owned packed-pixel frame. private ImageFrame ConvertToFrame( Av1FrameBuffer frameBuffer, + ObuFrameHeader frameHeader, CicpProfile effectiveColorProfile, Size presentationSize = default) where TPixel : unmanaged, IPixel @@ -327,8 +337,8 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable try { Size codedSize = new( - this.FrameHeader!.FrameSize.SuperResolutionUpscaledWidth, - this.FrameHeader.FrameSize.FrameHeight); + frameHeader.FrameSize.SuperResolutionUpscaledWidth, + frameHeader.FrameSize.FrameHeight); // A selected lower spatial layer can only be scaled upward to the image item's ispe extent here. // Other item-size corrections keep using the shared packed-pixel presentation path after decoding. @@ -452,6 +462,24 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable Av1CodecConfiguration? codecConfiguration, out CicpProfile effectiveColorProfile, Av1LayeredImageIndex? layeredImageIndex = null) + => this.DecodeFrameBuffer( + buffer, + containerColorProfile, + codecConfiguration, + out effectiveColorProfile, + out _, + layeredImageIndex); + + /// + /// Parses every coded frame in an AV1 payload and returns the final shown frame's native planes and header. + /// + private Av1FrameBuffer DecodeFrameBuffer( + Span buffer, + CicpProfile? containerColorProfile, + Av1CodecConfiguration? codecConfiguration, + out CicpProfile effectiveColorProfile, + out ObuFrameHeader frameHeader, + Av1LayeredImageIndex? layeredImageIndex) { effectiveColorProfile = this.DecodePayload( buffer, @@ -461,6 +489,7 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable requireShownFrame: true); using Av1ReferenceFrame outputFrame = this.referenceFrames.TakeOutput(); + frameHeader = outputFrame.FrameHeader; return outputFrame.TakeFrameBuffer(); } @@ -483,8 +512,13 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable this.codecConfiguration = codecConfiguration; this.containerColorProfile = containerColorProfile; this.validatedSequenceHeader = null; + this.obuReader.ResetMetadata(); this.SequenceHeader = null; this.FrameHeader = null; + this.DecodedInterPredictionFeatures = Av1InterPredictionFeatures.None; + + // Full tile syntax describes only frames reconstructed by this payload. Reference slots already own the compact + // state needed by later frames, so release the previous payload's reconstruction graph before parsing the next. this.FrameInfo?.ReleaseOwner(); this.FrameInfo = null; @@ -493,7 +527,7 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable if (layeredImageIndex is null) { Av1BitStreamReader reader = new(buffer); - this.obuReader.ReadAll(ref reader, buffer.Length, () => this, false); + this.obuReader.ReadAll(ref reader, buffer.Length, this, false); } else { @@ -514,7 +548,7 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable int layerSize = (int)declaredLayerSize; 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; } @@ -522,17 +556,16 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable { Span finalLayer = buffer[layerOffset..]; 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!; - Guard.NotNull(sequenceHeader, nameof(sequenceHeader)); + ObuSequenceHeader sequenceHeader = this.obuReader.SequenceHeader + ?? throw new InvalidImageContentException("The AV1 payload contains no sequence header."); + if (requireShownFrame) { - Guard.NotNull(this.referenceFrames.OutputFrame, nameof(this.referenceFrames.OutputFrame)); - Guard.NotNull(this.SequenceHeader, nameof(this.SequenceHeader)); - Guard.NotNull(this.FrameHeader, nameof(this.FrameHeader)); + _ = this.referenceFrames.ResolveOutput(); } // 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 // published CDF snapshots. None can be reused after a non-transactional frame transition has failed. - this.frameDecoder?.Dispose(); - this.frameDecoder = null; - this.frameBuffer?.Dispose(); - this.frameBuffer = null; - this.tileReader?.Dispose(); - this.tileReader = null; + this.frameDecodeState?.Dispose(); + this.frameDecodeState = null; this.obuReader.Reset(); this.entropyContexts?.Reset(); this.entropySequenceHeader = null; this.SequenceHeader = null; this.FrameHeader = null; + this.DecodedInterPredictionFeatures = Av1InterPredictionFeatures.None; this.FrameInfo?.ReleaseOwner(); this.FrameInfo = null; throw; @@ -584,6 +614,11 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable return; } + Av1FrameBuffer.ValidateDimensions( + sequenceHeader, + sequenceHeader.ColorConfig.GetColorFormat(), + false); + this.codecConfiguration?.Validate(sequenceHeader); CicpProfile? colorProfile = this.containerColorProfile; @@ -640,12 +675,11 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable /// The raster-order tile index. public void ReadTile(Span tileData, int tileNum) { - if (this.tileReader is null) + FrameDecodeState frameDecodeState; + if (this.frameDecodeState is null) { - ObuSequenceHeader? sequenceHeader = this.obuReader.SequenceHeader; - ObuFrameHeader? frameHeader = this.obuReader.FrameHeader; - Guard.NotNull(sequenceHeader, nameof(sequenceHeader)); - Guard.NotNull(frameHeader, nameof(frameHeader)); + ObuSequenceHeader sequenceHeader = this.obuReader.CurrentSequenceHeader; + ObuFrameHeader frameHeader = this.obuReader.CurrentFrameHeader; this.ValidateSequence(sequenceHeader); if (!ReferenceEquals(this.entropySequenceHeader, sequenceHeader)) @@ -680,27 +714,65 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable Av1FrameEntropyContexts entropyContexts = this.entropyContexts ??= new(frameHeader.QuantizationParameters.BaseQIndex); - this.tileReader = new Av1TileReader( - this.configuration, - sequenceHeader, - frameHeader, - entropyContexts, - primaryReferenceContext, - this.referenceFrames, - this.lumaPaletteColorIndexMap, - this.chromaPaletteColorIndexMap); - - this.frameBuffer = new Av1FrameBuffer(this.configuration, sequenceHeader, sequenceHeader.ColorConfig.GetColorFormat(), false) + Av1TileReader? tileReader = null; + Av1FrameBuffer? frameBuffer = null; + + // Presentation-only samples contain no new tile syntax, so they keep the most recently reconstructed + // frame state. Release that state only when a new reconstruction begins to avoid overlapping two graphs. + this.FrameInfo?.ReleaseOwner(); + this.FrameInfo = null; + + try { - Width = frameHeader.FrameSize.FrameWidth, - Height = frameHeader.FrameSize.FrameHeight - }; + tileReader = new Av1TileReader( + this.configuration, + sequenceHeader, + frameHeader, + entropyContexts, + primaryReferenceContext, + this.referenceFrames, + this.lumaPaletteColorIndexMap, + this.chromaPaletteColorIndexMap); + + frameBuffer = new Av1FrameBuffer( + 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); - this.tileReader.FrameDecoder = this.frameDecoder; + Av1FrameDecoder frameDecoder = new( + sequenceHeader, + frameHeader, + tileReader.FrameInfo, + frameBuffer, + this.referenceFrames, + new Av1TileReader.PaletteColorIndexMaps( + this.lumaPaletteColorIndexMap, + this.chromaPaletteColorIndexMap)); + + tileReader.FrameDecoder = frameDecoder; + frameDecodeState = new(tileReader, frameBuffer, frameDecoder); + this.frameDecodeState = frameDecodeState; + } + catch + { + frameBuffer?.Dispose(); + tileReader?.Dispose(); + throw; + } + } + else + { + frameDecodeState = this.frameDecodeState.Value; } - this.tileReader.ReadTile(tileData, tileNum); + frameDecodeState.TileReader.ReadTile(tileData, tileNum); } /// @@ -708,12 +780,15 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable /// public void CompleteFrame() { - ObuSequenceHeader sequenceHeader = this.obuReader.SequenceHeader!; - ObuFrameHeader frameHeader = this.obuReader.FrameHeader!; - Av1FrameBuffer? frameBuffer = this.frameBuffer; - this.frameBuffer = null; - Av1FrameDecoder? frameDecoder = this.frameDecoder; - this.frameDecoder = null; + ObuSequenceHeader sequenceHeader = this.obuReader.SequenceHeader + ?? throw new InvalidImageContentException("An AV1 frame cannot complete before its sequence header."); + + ObuFrameHeader frameHeader = this.obuReader.FrameHeader + ?? throw new InvalidImageContentException("An AV1 frame cannot complete before its frame header."); + + Av1FrameBuffer? frameBuffer = null; + Av1FrameDecoder? frameDecoder = null; + Av1TileReader? tileReader = null; Av1FrameBuffer? presentationBuffer = null; try @@ -724,7 +799,6 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable { Av1ReferenceFrame existingFrame = this.referenceFrames.ShowExisting((int)frameHeader.FrameToShowMapIdx); ObuFrameHeader existingFrameHeader = existingFrame.FrameHeader; - Av1FrameInfo existingFrameInfo = existingFrame.FrameInfo; if (existingFrameHeader.FrameType == ObuFrameType.KeyFrame) { @@ -739,33 +813,42 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable presentationBuffer = new Av1FrameBuffer( this.configuration, sequenceHeader, - sequenceHeader.ColorConfig.GetColorFormat(), - false); + existingFrame.FrameBuffer.ColorFormat, + false, + existingFrame.FrameBuffer.MaxWidth, + existingFrame.FrameBuffer.MaxHeight); // Retained reference samples remain ungrained. Existing-frame presentation receives its own // allocator-owned copy only when the inherited film-grain parameters actually modify the output. - existingFrame.FrameBuffer.CopyTo(presentationBuffer); + existingFrame.FrameBuffer.CopyVisibleTo(presentationBuffer); Av1FilmGrainDecoder filmGrainDecoder = new(sequenceHeader, existingFrameHeader, presentationBuffer); filmGrainDecoder.DecodeFrame(); - Av1ReferenceFrame presentationFrame = new(presentationBuffer, existingFrameHeader, existingFrameInfo); + Av1ReferenceFrame presentationFrame = new(presentationBuffer, existingFrameHeader); presentationBuffer = null; this.referenceFrames.CommitOutput(presentationFrame); } this.SequenceHeader = sequenceHeader; this.FrameHeader = existingFrameHeader; - - existingFrameInfo.AddOwner(); - this.FrameInfo?.ReleaseOwner(); - this.FrameInfo = existingFrameInfo; 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; - Av1FrameBuffer reconstructedFrameBuffer = frameBuffer!; - frameDecoder!.CompleteFrame(); + Av1FrameBuffer reconstructedFrameBuffer = frameBuffer; + frameDecoder.CompleteFrame(); bool retainsReference = (frameHeader.RefreshFrameFlags & byte.MaxValue) != 0; 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 // all in-loop filters lets every later block use the full padded span without per-prediction edge copies. Av1ReferenceFrameBorder.Extend(reconstructedFrameBuffer); + + // Detach only the state libaom retains on RefCntBuffer before any later ownership transfer can fail. + // The full reconstruction graph remains local to the current result and expires independently. + frameInfo.PrepareReferenceState(); } bool needsSeparatePresentation = frameHeader.ShowFrame && frameHeader.FilmGrainParameters.ApplyGrain && retainsReference; @@ -781,12 +868,14 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable presentationBuffer = new Av1FrameBuffer( this.configuration, sequenceHeader, - sequenceHeader.ColorConfig.GetColorFormat(), - false); + reconstructedFrameBuffer.ColorFormat, + false, + reconstructedFrameBuffer.MaxWidth, + reconstructedFrameBuffer.MaxHeight); // Film grain must never contaminate a decoded reference. A shown frame that is also refreshed therefore // receives one allocator-owned presentation copy; frames with no reference role are grained in place. - reconstructedFrameBuffer.CopyTo(presentationBuffer); + reconstructedFrameBuffer.CopyVisibleTo(presentationBuffer); } Av1FrameBuffer grainTarget = presentationBuffer ?? reconstructedFrameBuffer; @@ -799,14 +888,24 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable Av1ReferenceFrame referenceFrame; if (retainsReference) { - Av1FrameEntropyContexts entropyContexts = this.entropyContexts!; + Av1FrameEntropyContexts entropyContexts = tileReader.EntropyContexts; + 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 { // 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; @@ -818,34 +917,32 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable if (presentationBuffer is not null) { - Av1ReferenceFrame presentationFrame = new(presentationBuffer, frameHeader, frameInfo); + Av1ReferenceFrame presentationFrame = new(presentationBuffer, frameHeader); presentationBuffer = null; this.referenceFrames.CommitOutput(presentationFrame); } - if (frameHeader.ShowFrame) - { - this.SequenceHeader = sequenceHeader; - this.FrameHeader = frameHeader; - - // The output frame owner is released after its sample buffer transfers to the caller. Retain the - // parsed state independently so diagnostics and conformance inspection remain valid until the next - // bounded decode or decoder disposal. - frameInfo.AddOwner(); - this.FrameInfo?.ReleaseOwner(); - this.FrameInfo = frameInfo; - } + this.SequenceHeader = sequenceHeader; + this.FrameHeader = frameHeader; + this.DecodedInterPredictionFeatures |= frameInfo.InterPredictionFeatures; + + // Hidden frames can contain the inter syntax needed to validate a sequence. Retain only the latest full + // reconstruction state until the next bounded decode; reference-map entries keep their compact state. + frameInfo.AddOwner(); + this.FrameInfo?.ReleaseOwner(); + this.FrameInfo = frameInfo; } finally { - // A non-shown frame or failed reconstruction never escapes this callback. The tile reader releases only - // its initial frame-state lease; retained frames and the decoder result keep allocator-owned motion fields - // alive independently after the entropy-neighbor contexts are returned. + // A non-shown frame or failed reconstruction never escapes this callback. The tile reader releases the + // reconstruction lease; a retained frame keeps only its compact reference state after neighbor contexts + // and the remaining frame-sized syntax are returned. frameDecoder?.Dispose(); presentationBuffer?.Dispose(); frameBuffer?.Dispose(); - this.tileReader?.Dispose(); - this.tileReader = null; + tileReader?.Dispose(); + this.frameDecodeState?.Dispose(); + this.frameDecodeState = null; } } @@ -854,16 +951,46 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable /// public void Dispose() { - this.frameDecoder?.Dispose(); - this.frameDecoder = null; - this.frameBuffer?.Dispose(); - this.frameBuffer = null; - this.tileReader?.Dispose(); - this.tileReader = null; + this.frameDecodeState?.Dispose(); + this.frameDecodeState = null; this.referenceFrames.Dispose(); this.FrameInfo?.ReleaseOwner(); this.FrameInfo = null; this.lumaPaletteColorIndexMap.Dispose(); this.chromaPaletteColorIndexMap.Dispose(); } + + /// + /// Carries the active frame resources as one valid state so no partially initialized combination can be observed. + /// + private readonly struct FrameDecodeState( + Av1TileReader tileReader, + Av1FrameBuffer frameBuffer, + Av1FrameDecoder frameDecoder) : IDisposable + { + /// + /// Gets the tile parser shared by all tile groups in the frame. + /// + public Av1TileReader TileReader { get; } = tileReader; + + /// + /// Gets the destination sample buffer reconstructed by the frame pipeline. + /// + public Av1FrameBuffer FrameBuffer { get; } = frameBuffer; + + /// + /// Gets the reconstruction pipeline for the frame. + /// + public Av1FrameDecoder FrameDecoder { get; } = frameDecoder; + + /// + /// Releases every resource when ownership has not transferred to a completed frame. + /// + public void Dispose() + { + this.FrameDecoder.Dispose(); + this.FrameBuffer.Dispose(); + this.TileReader.Dispose(); + } + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs b/src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs index f54ad5946..98ff939cc 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs +++ b/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. /// /// The unmanaged storage-element type used by the plane allocations. -internal class Av1FrameBuffer : IDisposable +internal sealed class Av1FrameBuffer : IDisposable where T : unmanaged { /// @@ -53,7 +53,12 @@ internal class Av1FrameBuffer : IDisposable private readonly int storageElementsPerSample; /// - /// Initializes a new instance of the class. + /// The complete plane ownership state, or after disposal. + /// + private FramePlanes? planes; + + /// + /// Initializes a new instance of the class at sequence-maximum dimensions. /// /// The configuration providing the plane allocator. /// The sequence header defining maximum dimensions, bit depth, and chroma layout. @@ -61,11 +66,40 @@ internal class Av1FrameBuffer : IDisposable /// Indicates whether reconstruction uses native 16-bit sample storage. /// The padded frame planes cannot be represented as contiguous allocations. public Av1FrameBuffer(Configuration configuration, ObuSequenceHeader sequenceHeader, Av1ColorFormat maxColorFormat, bool is16BitPipeline) + : this( + configuration, + sequenceHeader, + maxColorFormat, + is16BitPipeline, + sequenceHeader.MaxFrameWidth, + sequenceHeader.MaxFrameHeight) { + } + + /// + /// Initializes a new instance of the class for one active frame allocation. + /// + /// The configuration providing the plane allocator. + /// The sequence header defining bit depth and chroma layout. + /// The color format to allocate for a non-monochrome sequence. + /// Indicates whether reconstruction uses native 16-bit sample storage. + /// The padded plane's active luma width before decoder borders. + /// The padded plane's active luma height before decoder borders. + /// The padded frame planes cannot be represented as contiguous allocations. + public Av1FrameBuffer( + Configuration configuration, + ObuSequenceHeader sequenceHeader, + Av1ColorFormat maxColorFormat, + bool is16BitPipeline, + int allocationWidth, + int allocationHeight) + { + ValidateDimensions(sequenceHeader, maxColorFormat, is16BitPipeline); + this.MemoryAllocator = configuration.MemoryAllocator; Av1ColorFormat colorFormat = sequenceHeader.ColorConfig.IsMonochrome ? Av1ColorFormat.Yuv400 : maxColorFormat; - this.MaxWidth = sequenceHeader.MaxFrameWidth; - this.MaxHeight = sequenceHeader.MaxFrameHeight; + this.MaxWidth = allocationWidth; + this.MaxHeight = allocationHeight; this.BitDepth = sequenceHeader.ColorConfig.BitDepth; this.ColorConfig = sequenceHeader.ColorConfig; this.BytesPerSample = this.BitDepth > Av1BitDepth.EightBit || is16BitPipeline ? 2 : 1; @@ -121,34 +155,42 @@ internal class Av1FrameBuffer : IDisposable 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 // stride. Establish that invariant at the plane owner instead of copying fragmented groups in every hot path. + Buffer2D luma = configuration.MemoryAllocator.Allocate2D( + strideY * this.storageElementsPerSample, + heightY, + preferContiguosImageBuffers: true); + + Buffer2D? chromaBlue = null; + Buffer2D? chromaRed = null; try { - if ((bufferEnableMask & PictureBufferYFlag) != 0) + ChromaPlanes? chroma = null; + if (bufferEnableMask == PictureBufferFullMask) { - this.BufferY = configuration.MemoryAllocator.Allocate2D(strideY * this.storageElementsPerSample, heightY, preferContiguosImageBuffers: true); - } + chromaBlue = configuration.MemoryAllocator.Allocate2D( + strideChroma * this.storageElementsPerSample, + heightChroma, + preferContiguosImageBuffers: true); - if ((bufferEnableMask & PictureBufferCbFlag) != 0) - { - this.BufferCb = configuration.MemoryAllocator.Allocate2D(strideChroma * this.storageElementsPerSample, heightChroma, preferContiguosImageBuffers: true); - } + chromaRed = configuration.MemoryAllocator.Allocate2D( + strideChroma * this.storageElementsPerSample, + heightChroma, + preferContiguosImageBuffers: true); - if ((bufferEnableMask & PictureBufferCrFlag) != 0) - { - this.BufferCr = configuration.MemoryAllocator.Allocate2D(strideChroma * this.storageElementsPerSample, heightChroma, preferContiguosImageBuffers: true); + chroma = new ChromaPlanes(chromaBlue, chromaRed); } + + this.planes = new(luma, chroma); } catch { // Construction publishes the owner only after every required plane has been rented. Release earlier planes // here because a later allocation failure leaves no constructed frame buffer for the caller to dispose. - this.Dispose(); + chromaRed?.Dispose(); + chromaBlue?.Dispose(); + luma.Dispose(); throw; } } @@ -161,17 +203,17 @@ internal class Av1FrameBuffer : IDisposable /// /// Gets the Y luma buffer. /// - public Buffer2D? BufferY { get; private set; } + public Buffer2D? BufferY => this.planes?.Luma; /// /// Gets the U chroma buffer. /// - public Buffer2D? BufferCb { get; private set; } + public Buffer2D? BufferCb => this.planes?.Chroma?.Blue; /// /// Gets the V chroma buffer. /// - public Buffer2D? BufferCr { get; private set; } + public Buffer2D? BufferCr => this.planes?.Chroma?.Red; /// /// Gets or sets the horizontal padding distance. @@ -234,22 +276,71 @@ internal class Av1FrameBuffer : IDisposable public MemoryAllocator MemoryAllocator { get; } /// - /// 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. /// - /// The frame buffer receiving the copied reconstruction. - public void CopyTo(Av1FrameBuffer destination) + /// The sequence header defining maximum dimensions, bit depth, and chroma layout. + /// The maximum color format required by the sequence. + /// Indicates whether reconstruction uses native 16-bit sample storage. + 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 - // presentation consumes right and bottom padding for odd dimensions, so copying only visible rows would leave - // part of the independently owned presentation surface undefined. - this.BufferY!.DangerousGetSingleSpan().CopyTo(destination.BufferY!.DangerousGetSingleSpan()); - Buffer2D? chromaBlue = this.BufferCb; - if (chromaBlue is not null) + int bytesPerSample = sequenceHeader.ColorConfig.BitDepth > Av1BitDepth.EightBit || is16BitPipeline ? 2 : 1; + int storageElementsPerSample = Math.Max( + (bytesPerSample + Unsafe.SizeOf() - 1) / Unsafe.SizeOf(), + 1); + + long strideY = (long)sequenceHeader.MaxFrameWidth + (DecoderPaddingValue * 2L); + long heightY = (long)sequenceHeader.MaxFrameHeight + (DecoderPaddingValue * 2L); + Av1ColorFormat colorFormat = sequenceHeader.ColorConfig.IsMonochrome ? Av1ColorFormat.Yuv400 : maxColorFormat; + long strideChroma = 0; + long heightChroma = 0; + + switch (colorFormat) { - chromaBlue.DangerousGetSingleSpan().CopyTo(destination.BufferCb!.DangerousGetSingleSpan()); - this.BufferCr!.DangerousGetSingleSpan().CopyTo(destination.BufferCr!.DangerousGetSingleSpan()); + case Av1ColorFormat.Yuv420: + strideChroma = (strideY + 1) >> 1; + heightChroma = (heightY + 1) >> 1; + break; + case Av1ColorFormat.Yuv422: + strideChroma = (strideY + 1) >> 1; + heightChroma = heightY; + break; + case Av1ColorFormat.Yuv444: + strideChroma = strideY; + heightChroma = heightY; + break; + } + + long lumaElementCount = strideY * storageElementsPerSample * heightY; + long chromaElementCount = strideChroma * storageElementsPerSample * heightChroma; + if (lumaElementCount >= int.MaxValue || + (!sequenceHeader.ColorConfig.IsMonochrome && chromaElementCount >= int.MaxValue)) + { + // Every decoder operator addresses padding through one contiguous span. Reject external sequence geometry + // before frame-wide syntax state is allocated so hostile dimensions cannot bypass allocator limits. + throw new InvalidImageContentException("The AV1 frame dimensions exceed the contiguous decoder plane limit."); } + } + + /// + /// Gets the padded storage allocation for one component plane. + /// + /// The requested component plane. + /// The requested plane allocation. + public Buffer2D GetPlaneBuffer(Av1Plane plane) + { + this.GetPlaneLayout(plane, 0, 0, out Buffer2D buffer, out _, out _, out _, out _); + return buffer; + } + /// + /// Copies the visible sample planes and active picture geometry to another compatible frame buffer. + /// + /// The frame buffer receiving the copied reconstruction. + public void CopyVisibleTo(Av1FrameBuffer destination) + { destination.StartPosition = this.StartPosition; destination.OriginX = this.OriginX; destination.OriginY = this.OriginY; @@ -259,6 +350,47 @@ internal class Av1FrameBuffer : IDisposable destination.MaxHeight = this.MaxHeight; destination.BitDepth = this.BitDepth; destination.ColorFormat = this.ColorFormat; + + int planeCount = this.ColorFormat == Av1ColorFormat.Yuv400 ? 1 : 3; + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) + { + Av1Plane plane = (Av1Plane)planeIndex; + int subX = plane != Av1Plane.Y && this.ColorFormat is Av1ColorFormat.Yuv420 or Av1ColorFormat.Yuv422 ? 1 : 0; + int subY = plane != Av1Plane.Y && this.ColorFormat == Av1ColorFormat.Yuv420 ? 1 : 0; + + this.GetPlaneLayout( + plane, + subX, + subY, + out Buffer2D sourceBuffer, + out int sourceOriginX, + out int sourceOriginY, + out int width, + out int height); + + destination.GetPlaneLayout( + plane, + subX, + subY, + out Buffer2D 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)); + } + } } /// @@ -266,12 +398,21 @@ internal class Av1FrameBuffer : IDisposable /// public void Dispose() { - this.BufferY?.Dispose(); - this.BufferY = null; - this.BufferCb?.Dispose(); - this.BufferCb = null; - this.BufferCr?.Dispose(); - this.BufferCr = null; + FramePlanes? ownedPlanes = this.planes; + this.planes = null; + if (ownedPlanes is null) + { + return; + } + + FramePlanes activePlanes = ownedPlanes.Value; + activePlanes.Luma.Dispose(); + ChromaPlanes? chroma = activePlanes.Chroma; + if (chroma is not null) + { + chroma.Value.Blue.Dispose(); + chroma.Value.Red.Dispose(); + } } /// @@ -447,19 +588,23 @@ internal class Av1FrameBuffer : IDisposable out int width, out int height) { + FramePlanes? ownedPlanes = this.planes; + ObjectDisposedException.ThrowIf(ownedPlanes is null, this); + + FramePlanes activePlanes = ownedPlanes.Value; switch (plane) { case Av1Plane.Y: - Guard.NotNull(this.BufferY); - buffer = this.BufferY; + buffer = activePlanes.Luma; originX = this.OriginX; originY = this.OriginY; width = this.Width; height = this.Height; break; case Av1Plane.U: - Guard.NotNull(this.BufferCb); - buffer = this.BufferCb; + buffer = activePlanes.Chroma?.Blue + ?? throw new InvalidOperationException("A monochrome AV1 frame has no blue-difference plane."); + originX = this.OriginX >> subX; originY = this.OriginY >> subY; width = Av1Math.DivideLog2Ceiling(this.Width, subX); @@ -467,8 +612,9 @@ internal class Av1FrameBuffer : IDisposable break; case Av1Plane.V: default: - Guard.NotNull(this.BufferCr); - buffer = this.BufferCr; + buffer = activePlanes.Chroma?.Red + ?? throw new InvalidOperationException("A monochrome AV1 frame has no red-difference plane."); + originX = this.OriginX >> subX; originY = this.OriginY >> subY; width = Av1Math.DivideLog2Ceiling(this.Width, subX); @@ -476,4 +622,33 @@ internal class Av1FrameBuffer : IDisposable break; } } + + /// + /// Carries the mandatory luma owner and the optional complete chroma pair as one state. + /// + private readonly struct FramePlanes(Buffer2D luma, ChromaPlanes? chroma) + { + /// + /// Gets the padded luma plane. + /// + public Buffer2D Luma { get; } = luma; + + /// + /// Gets the padded chroma planes when the frame contains chroma. + /// + public ChromaPlanes? Chroma { get; } = chroma; + } + + private readonly struct ChromaPlanes(Buffer2D blue, Buffer2D red) + { + /// + /// Gets the padded blue-difference plane. + /// + public Buffer2D Blue { get; } = blue; + + /// + /// Gets the padded red-difference plane. + /// + public Buffer2D Red { get; } = red; + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Color/Av1PlanarSampleBuffer.cs b/src/ImageSharp/Formats/Heif/Av1/Color/Av1PlanarSampleBuffer.cs index 8fe0bab4e..6e7bdb627 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Color/Av1PlanarSampleBuffer.cs +++ b/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. /// /// The native unsigned sample storage type. -internal struct Av1PlanarSampleBuffer : IHeifPlanarSampleBuffer +internal readonly struct Av1PlanarSampleBuffer : IHeifPlanarSampleBuffer where TSample : unmanaged { /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBuffer.cs b/src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBuffer.cs index 9a69c7285..68660a47b 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBuffer.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBuffer.cs @@ -25,19 +25,9 @@ internal sealed class Av1PresentationSampleBuffer : IDisposabl private readonly MemoryAllocator memoryAllocator; /// - /// The scaled luma plane. + /// The complete set of owned presentation planes, or after disposal. /// - private Buffer2D? luma; - - /// - /// The scaled blue-difference plane. - /// - private Buffer2D? chromaBlue; - - /// - /// The scaled red-difference plane. - /// - private Buffer2D? chromaRed; + private PresentationPlanes? planes; /// /// Initializes a new instance of the class. @@ -64,23 +54,34 @@ internal sealed class Av1PresentationSampleBuffer : IDisposabl int destinationChromaWidth = DivideCeiling(width, 1 << source.ChromaSubsamplingX); int destinationChromaHeight = DivideCeiling(height, 1 << source.ChromaSubsamplingY); + Buffer2D? luma = null; + Buffer2D? chromaBlue = null; + Buffer2D? chromaRed = null; try { - this.luma = this.memoryAllocator.Allocate2D(width, height); - this.ScalePlane(source, Av1Plane.Y, source.Width, source.Height, this.luma); + luma = this.memoryAllocator.Allocate2D(width, height); + this.ScalePlane(source, Av1Plane.Y, source.Width, source.Height, luma); + + ChromaPlanes? chroma = null; if (!source.IsMonochrome) { - this.chromaBlue = this.memoryAllocator.Allocate2D(destinationChromaWidth, destinationChromaHeight); - this.ScalePlane(source, Av1Plane.U, sourceChromaWidth, sourceChromaHeight, this.chromaBlue); + chromaBlue = this.memoryAllocator.Allocate2D(destinationChromaWidth, destinationChromaHeight); + this.ScalePlane(source, Av1Plane.U, sourceChromaWidth, sourceChromaHeight, chromaBlue); - this.chromaRed = this.memoryAllocator.Allocate2D(destinationChromaWidth, destinationChromaHeight); - this.ScalePlane(source, Av1Plane.V, sourceChromaWidth, sourceChromaHeight, this.chromaRed); + chromaRed = this.memoryAllocator.Allocate2D(destinationChromaWidth, destinationChromaHeight); + this.ScalePlane(source, Av1Plane.V, sourceChromaWidth, sourceChromaHeight, chromaRed); + chroma = new ChromaPlanes(chromaBlue, chromaRed); } + + // Publish ownership only after every required plane has been allocated and initialized. + this.planes = new PresentationPlanes(luma, chroma); } catch { - this.Dispose(); + luma?.Dispose(); + chromaBlue?.Dispose(); + chromaRed?.Dispose(); throw; } } @@ -140,12 +141,21 @@ internal sealed class Av1PresentationSampleBuffer : IDisposabl /// public void Dispose() { - this.luma?.Dispose(); - this.chromaBlue?.Dispose(); - this.chromaRed?.Dispose(); - this.luma = null; - this.chromaBlue = null; - this.chromaRed = null; + PresentationPlanes? planes = this.planes; + this.planes = null; + + if (planes is null) + { + return; + } + + planes.Value.Luma.Dispose(); + ChromaPlanes? chroma = planes.Value.Chroma; + if (chroma is not null) + { + chroma.Value.Blue.Dispose(); + chroma.Value.Red.Dispose(); + } } /// @@ -155,13 +165,22 @@ internal sealed class Av1PresentationSampleBuffer : IDisposabl /// The zero-based plane row. /// The visible samples in the requested row. public Span GetRowSpan(Av1Plane plane, int row) - => plane switch + { + PresentationPlanes planes = this.planes + ?? throw new ObjectDisposedException(nameof(Av1PresentationSampleBuffer)); + + Buffer2D buffer = plane switch { - Av1Plane.Y => this.luma!.DangerousGetRowSpan(row), - Av1Plane.U => this.chromaBlue!.DangerousGetRowSpan(row), - _ => this.chromaRed!.DangerousGetRowSpan(row) + Av1Plane.Y => planes.Luma, + Av1Plane.U => planes.Chroma?.Blue + ?? throw new InvalidOperationException("The AV1 presentation buffer has no blue-difference plane."), + _ => planes.Chroma?.Red + ?? throw new InvalidOperationException("The AV1 presentation buffer has no red-difference plane.") }; + return buffer.DangerousGetRowSpan(row); + } + /// /// Scales one component plane with the native integer filter used by pinned libavif's libyuv backend. /// @@ -742,4 +761,18 @@ internal sealed class Av1PresentationSampleBuffer : IDisposabl /// The positive divisor. /// The ceiling-rounded quotient. private static int DivideCeiling(int value, int divisor) => (value + divisor - 1) / divisor; + + private readonly struct ChromaPlanes(Buffer2D blue, Buffer2D red) + { + public Buffer2D Blue { get; } = blue; + + public Buffer2D Red { get; } = red; + } + + private readonly struct PresentationPlanes(Buffer2D luma, ChromaPlanes? chroma) + { + public Buffer2D Luma { get; } = luma; + + public ChromaPlanes? Chroma { get; } = chroma; + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs index f92a5bde1..c56744fb0 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; /// /// Stores and adapts an AV1 inverse cumulative distribution used by the range coder. /// -internal class Av1Distribution +internal sealed class Av1Distribution { /// /// The exclusive upper bound of the Q15 probability domain. @@ -271,7 +271,7 @@ internal class Av1Distribution /// /// The forward Q15 thresholds followed by a zero sentinel slot. /// The symbol-count contribution to the update rate. - private Av1Distribution(uint[] props, int speed) + private Av1Distribution(ReadOnlySpan props, int speed) { this.probabilities = new uint[props.Length]; diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContexts.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContexts.cs index 476218527..c180fad59 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContexts.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContexts.cs @@ -21,11 +21,6 @@ internal sealed class Av1FrameEntropyContexts /// private InlineArray10 returnedSnapshots; - /// - /// The number of returned snapshot graphs currently available for reuse. - /// - private int returnedSnapshotCount; - /// /// The base quantizer index used to initialize a newly required snapshot graph. /// @@ -105,20 +100,24 @@ internal sealed class Av1FrameEntropyContexts /// The snapshot that must later be returned through . public Av1FrameEntropyContext RentPublishedSnapshot() { - Av1FrameEntropyContext snapshot; - if (this.returnedSnapshotCount == 0) - { - // 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 + Av1FrameEntropyContext? snapshot = null; + for (int snapshotIndex = 0; snapshotIndex < MaximumSnapshotCount; snapshotIndex++) { - int snapshotIndex = --this.returnedSnapshotCount; - snapshot = this.returnedSnapshots[snapshotIndex]!; + Av1FrameEntropyContext? returnedSnapshot = this.returnedSnapshots[snapshotIndex]; + if (returnedSnapshot is null) + { + continue; + } + this.returnedSnapshots[snapshotIndex] = null; + snapshot = returnedSnapshot; + break; } + // Eight slots can own distinct frames while the selected output owns a ninth frame no longer present in + // the map. Rent one further graph before commit releases the owner displaced by the completed frame. + snapshot ??= new(this.currentQIndex); + this.Published.SnapshotTo(snapshot); return snapshot; } @@ -130,9 +129,15 @@ internal sealed class Av1FrameEntropyContexts public void ReturnSnapshot(Av1FrameEntropyContext snapshot) { // The fixed capacity covers eight distinct slot owners, one detached presentation owner, and the replacement - // frame rented before commit. Av1ReferenceFrame returns each graph exactly once, so the session cannot exceed - // this bound. - this.returnedSnapshots[this.returnedSnapshotCount++] = snapshot; + // frame rented before commit. Av1ReferenceFrame returns each graph exactly once, so one slot is always free. + for (int snapshotIndex = 0; snapshotIndex < MaximumSnapshotCount; snapshotIndex++) + { + if (this.returnedSnapshots[snapshotIndex] is null) + { + this.returnedSnapshots[snapshotIndex] = snapshot; + return; + } + } } /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs index 4e1738fe1..9bd6cab70 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs +++ b/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; } - /// - /// Reads one coefficient base-range symbol. - /// - /// The square transform-size probability context. - /// The luma or chroma plane category. - /// The coefficient base-range context. - /// The decoded base-range symbol. - 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]); - } - /// /// Reads the sign of a nonzero DC coefficient. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorConfig.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorConfig.cs index c3de94a03..1b6f4b097 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorConfig.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorConfig.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Contains the color configuration signaled by an AV1 sequence header. /// -internal class ObuColorConfig +internal sealed class ObuColorConfig { /// /// Stores whether the sequence uses a single monochrome plane. diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuConstraintDirectionalEnhancementFilterParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuConstraintDirectionalEnhancementFilterParameters.cs index e4d6502d9..a8c7e8d9d 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuConstraintDirectionalEnhancementFilterParameters.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuConstraintDirectionalEnhancementFilterParameters.cs @@ -6,8 +6,18 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Contains the constrained directional enhancement filter parameters for an AV1 frame. /// -internal class ObuConstraintDirectionalEnhancementFilterParameters +internal sealed class ObuConstraintDirectionalEnhancementFilterParameters { + /// + /// Stores the fixed sixteen luma filter strengths without a per-frame array allocation. + /// + private InlineArray16 yStrength; + + /// + /// Stores the fixed sixteen chroma filter strengths without a per-frame array allocation. + /// + private InlineArray16 uvStrength; + /// /// Gets or sets the number of bits used to select a filter-strength entry. /// @@ -19,12 +29,12 @@ internal class ObuConstraintDirectionalEnhancementFilterParameters public int Damping { get; set; } = 3; /// - /// Gets or sets the primary and secondary luma strengths for each filter entry. + /// Gets the primary and secondary luma strengths for each filter entry. /// - public int[] YStrength { get; set; } = new int[16]; + public Span YStrength => this.yStrength; /// - /// Gets or sets the primary and secondary chroma strengths for each filter entry. + /// Gets the primary and secondary chroma strengths for each filter entry. /// - public int[] UvStrength { get; set; } = new int[16]; + public Span UvStrength => this.uvStrength; } diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDecoderModelInfo.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDecoderModelInfo.cs index 15c2b48c1..723db4046 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDecoderModelInfo.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDecoderModelInfo.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1; /// /// Contains the field widths and decoding-clock units used by the AV1 decoder model. /// -internal class ObuDecoderModelInfo +internal sealed class ObuDecoderModelInfo { /// /// Gets or sets BufferDelayLength. Specifies the length of the decoder_buffer_delay and the encoder_buffer_delay diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDeltaParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDeltaParameters.cs index a3e709c69..c35cbc11d 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDeltaParameters.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDeltaParameters.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Contains the delta-quantizer or delta-loop-filter signaling parameters for an AV1 frame. /// -internal class ObuDeltaParameters +internal sealed class ObuDeltaParameters { /// /// Gets or sets a value indicating whether per-block delta values are present. diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFilmGrainParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFilmGrainParameters.cs index b76d94785..8b9a5d706 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFilmGrainParameters.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFilmGrainParameters.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Contains the parameters used to synthesize AV1 film grain for a decoded frame. /// -internal class ObuFilmGrainParameters +internal sealed class ObuFilmGrainParameters { /// /// Stores the luma scaling-point coordinates without a per-frame array allocation. diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs index 86c4f42ac..99e62da16 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Stores the decoded syntax and derived state for one AV1 frame header. /// -internal class ObuFrameHeader +internal sealed class ObuFrameHeader { /// /// Stores the validity state of the eight reference-frame slots without a per-header array allocation. @@ -38,6 +38,11 @@ internal class ObuFrameHeader /// private InlineArray7 globalMotionParameters; + /// + /// Stores the lossless-coding flag for each of the eight segments without a per-header array allocation. + /// + private InlineArray8 losslessArray; + /// /// Gets or sets the temporal-layer identifier carried by the primary frame-header OBU. /// @@ -89,9 +94,9 @@ internal class ObuFrameHeader public bool CodedLossless { get; set; } /// - /// Gets or sets the lossless-coding flag for each segment. + /// Gets the mutable lossless-coding flags for each segment. /// - public bool[] LosslessArray { get; set; } = new bool[Av1Constants.MaxSegmentCount]; + public Span LosslessArray => this.losslessArray; /// /// Gets or sets the frame quantization parameters. diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameSize.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameSize.cs index 63c713e48..b5b1cab43 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameSize.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameSize.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Contains the coded, upscaled, and rendered dimensions of an AV1 frame. /// -internal class ObuFrameSize +internal sealed class ObuFrameSize { /// /// Gets or sets the coded frame width. diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuHeader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuHeader.cs index ed8deedcb..ec2d3e4f3 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuHeader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuHeader.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Contains the parsed header and payload size of an AV1 open bitstream unit. /// -internal class ObuHeader +internal sealed class ObuHeader { /// /// Gets or sets the number of bytes occupied by the fixed OBU header and optional extension. diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopFilterParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopFilterParameters.cs index f03d2cf4e..0f0c3f07e 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopFilterParameters.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopFilterParameters.cs @@ -6,22 +6,39 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Contains the AV1 deblocking-loop-filter parameters for a frame. /// -internal class ObuLoopFilterParameters +internal sealed class ObuLoopFilterParameters { + /// + /// Stores the horizontal and vertical luma filter levels. + /// + private InlineArray4 filterLevel; + + /// + /// Stores the fixed reference-frame delta table. + /// + private InlineArray8 referenceDeltas; + + /// + /// Stores the fixed prediction-mode delta table. + /// + private InlineArray4 modeDeltas; + /// /// Initializes a new instance of the class with the AV1 default reference and mode deltas. /// public ObuLoopFilterParameters() { // 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.ModeDeltas = [0, 0]; + this.referenceDeltas[0] = 1; + this.referenceDeltas[4] = -1; + this.referenceDeltas[6] = -1; + this.referenceDeltas[7] = -1; } /// - /// Gets or sets the horizontal and vertical luma filter levels. + /// Gets the horizontal and vertical luma filter levels. /// - public int[] FilterLevel { get; set; } = new int[2]; + public Span FilterLevel => this.filterLevel[..2]; /// /// Gets or sets the U-plane filter level. @@ -51,10 +68,10 @@ internal class ObuLoopFilterParameters /// /// Gets the filter-level deltas for the AV1 reference-frame categories. /// - public int[] ReferenceDeltas { get; } + public Span ReferenceDeltas => this.referenceDeltas; /// /// Gets the filter-level deltas for the AV1 prediction modes. /// - public int[] ModeDeltas { get; } + public Span ModeDeltas => this.modeDeltas[..2]; } diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationItem.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationItem.cs index b8d9a8f05..06585758f 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationItem.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationItem.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Contains the loop-restoration type and unit size for one color plane. /// -internal class ObuLoopRestorationItem +internal struct ObuLoopRestorationItem { /// /// Gets or sets the restoration-unit size, in samples. @@ -16,5 +16,5 @@ internal class ObuLoopRestorationItem /// /// Gets or sets the restoration filter type. /// - public ObuRestorationType Type { get; set; } = ObuRestorationType.None; + public ObuRestorationType Type { get; set; } } diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationParameters.cs index ad0cade7f..7c1ec6013 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationParameters.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationParameters.cs @@ -6,20 +6,12 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Contains the AV1 loop-restoration configuration for a frame. /// -internal class ObuLoopRestorationParameters +internal sealed class ObuLoopRestorationParameters { /// - /// Initializes a new instance of the class. + /// Stores the fixed three plane configurations without an outer array or per-plane object allocation. /// - public ObuLoopRestorationParameters() - { - // 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(); - } + private InlineArray4 items; /// /// Gets or sets a value indicating whether any plane uses loop restoration. @@ -34,7 +26,7 @@ internal class ObuLoopRestorationParameters /// /// Gets the loop-restoration configuration for each plane. /// - public ObuLoopRestorationItem[] Items { get; } + public Span Items => this.items[..3]; /// /// Gets or sets the luma restoration-unit size shift. diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOperatingPoint.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOperatingPoint.cs index a36aa68ca..b861c1a58 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOperatingPoint.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOperatingPoint.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Contains the sequence-level constraints for an AV1 operating point. /// -internal class ObuOperatingPoint +internal sealed class ObuOperatingPoint { /// /// Gets or sets the operating-point index. diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOrderHintInfo.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOrderHintInfo.cs index d8d0aacd6..ba4c193eb 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOrderHintInfo.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOrderHintInfo.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Contains the sequence-level order-hint and dependent prediction-tool settings. /// -internal class ObuOrderHintInfo +internal sealed class ObuOrderHintInfo { /// /// Gets or sets a value indicating whether order hints are enabled. diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuQuantizationParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuQuantizationParameters.cs index 412ea7ca1..92f685d46 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuQuantizationParameters.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuQuantizationParameters.cs @@ -6,17 +6,37 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Contains the AV1 quantization parameters for a frame. /// -internal class ObuQuantizationParameters +internal sealed class ObuQuantizationParameters { + /// + /// Stores the effective quantizer index for each of the eight segments without a per-header array allocation. + /// + private InlineArray8 qIndex; + + /// + /// Stores the three plane DC quantizer-index deltas without a per-frame array allocation. + /// + private InlineArray4 deltaQDc; + + /// + /// Stores the three plane AC quantizer-index deltas without a per-frame array allocation. + /// + private InlineArray4 deltaQAc; + + /// + /// Stores the three plane quantization-matrix levels without a per-frame array allocation. + /// + private InlineArray4 qMatrix; + /// /// Gets or sets the base quantizer index. /// public int BaseQIndex { get; set; } /// - /// Gets or sets the effective quantizer index for each segment. + /// Gets the mutable effective quantizer indices for each segment. /// - public int[] QIndex { get; set; } = new int[Av1Constants.MaxSegmentCount]; + public Span QIndex => this.qIndex; /// /// Gets or sets a value indicating whether quantization matrices are enabled. @@ -24,19 +44,19 @@ internal class ObuQuantizationParameters public bool IsUsingQMatrix { get; set; } /// - /// 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. /// - public int[] DeltaQDc { get; set; } = new int[3]; + public Span DeltaQDc => this.deltaQDc[..3]; /// - /// 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. /// - public int[] DeltaQAc { get; set; } = new int[3]; + public Span DeltaQAc => this.deltaQAc[..3]; /// - /// 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. /// - public int[] QMatrix { get; set; } = new int[3]; + public Span QMatrix => this.qMatrix[..3]; /// /// Gets or sets a value indicating whether the U and V planes use separate quantizer deltas. diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs index 4920687b7..0931f8ac4 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Parses AV1 open bitstream units and supplies decoded tile payloads to an AV1 tile reader. /// -internal class ObuReader +internal sealed class ObuReader { /// /// The number of bits used to address one of AV1's eight reference-map slots. @@ -80,11 +80,6 @@ internal class ObuReader /// private ObuFrameReferenceState frameReferenceState; - /// - /// The tile reader created for the current coded frame. - /// - private IAv1TileReader? decoder; - /// /// The temporal- and spatial-layer mask for the selected operating point. /// @@ -119,6 +114,18 @@ internal class ObuReader this.referenceFrames = referenceFrames; } + /// + /// Supplies a tile reader while preserving distinct fixed-reader and factory call contracts. + /// + private interface ITileReaderProvider + { + /// + /// Gets the tile reader for the current frame. + /// + /// The tile reader. + IAv1TileReader Get(); + } + /// /// Gets or sets the most recently parsed sequence header. /// @@ -129,6 +136,39 @@ internal class ObuReader /// public ObuFrameHeader? FrameHeader { get; set; } + /// + /// Gets content light-level metadata parsed from the current AV1 session. + /// + public HeifContentLightLevel? ContentLightLevel { get; private set; } + + /// + /// Gets mastering-display color-volume metadata parsed from the current AV1 session. + /// + public HeifMasteringDisplayColorVolume? MasteringDisplayColorVolume { get; private set; } + + /// + /// Gets the sequence header established before frame-dependent syntax is read. + /// + public ObuSequenceHeader CurrentSequenceHeader => + this.SequenceHeader + ?? throw new InvalidImageContentException("AV1 frame syntax appeared before a sequence header."); + + /// + /// Gets the frame header established before frame-dependent syntax is read. + /// + public ObuFrameHeader CurrentFrameHeader => + this.FrameHeader + ?? throw new InvalidImageContentException("AV1 tile syntax appeared before a frame header."); + + /// + /// Clears metadata retained from the preceding bounded AV1 image payload. + /// + public void ResetMetadata() + { + this.ContentLightLevel = null; + this.MasteringDisplayColorVolume = null; + } + /// /// Parses every open bitstream unit in one bounded AV1 payload. /// @@ -137,6 +177,40 @@ internal class ObuReader /// Creates one tile reader when the first tile payload of each coded frame is encountered. /// A value indicating whether each OBU is prefixed by an Annex B length field. public void ReadAll(ref Av1BitStreamReader reader, int dataSize, Func creator, bool isAnnexB = false) + { + ArgumentNullException.ThrowIfNull(creator); + + this.ReadAll(ref reader, dataSize, new TileReaderFactoryProvider(creator), isAnnexB); + } + + /// + /// Parses every open bitstream unit in one bounded AV1 payload using one existing tile reader. + /// + /// The reader positioned at the first OBU. + /// The number of bytes available for the bounded payload. + /// The tile reader used for each coded frame in the payload. + /// A value indicating whether each OBU is prefixed by an Annex B length field. + 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); + } + + /// + /// Parses every open bitstream unit in one bounded AV1 payload. + /// + /// The reader positioned at the first OBU. + /// The number of bytes available for the bounded payload. + /// The non-allocating tile-reader source used by this payload. + /// Provides one tile reader for each coded frame. + /// A value indicating whether each OBU is prefixed by an Annex B length field. + private void ReadAll( + ref Av1BitStreamReader reader, + int dataSize, + TTileReaderProvider tileReaderProvider, + bool isAnnexB) + where TTileReaderProvider : struct, ITileReaderProvider { bool completed = false; @@ -151,6 +225,7 @@ internal class ObuReader bool seenFrameHeader = false; int nextTileStart = 0; Span primaryFrameHeaderPayload = default; + IAv1TileReader? activeDecoder = null; while (dataSize > 0) { @@ -220,7 +295,7 @@ internal class ObuReader } Av1BitStreamReader payloadReader = new(obuPayload); - bool frameDecodingFinished = false; + IAv1TileReader? decoderToComplete = null; int decodedPayloadSize; switch (header.Type) @@ -276,8 +351,8 @@ internal class ObuReader if (primaryFrameHeader.ShowExistingFrame) { // This header completes by selecting retained samples; no tile group belongs to it. - this.decoder ??= creator(); - frameDecodingFinished = true; + activeDecoder ??= tileReaderProvider.Get(); + decoderToComplete = activeDecoder; } break; @@ -336,11 +411,16 @@ internal class ObuReader 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 // 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); break; 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. decodedPayloadSize = 0; 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: int lastNonzeroIndex = obuPayload.Length - 1; while (lastNonzeroIndex >= 0 && obuPayload[lastNonzeroIndex] == 0) @@ -375,9 +462,9 @@ internal class ObuReader decodedPayloadSize = payloadSize; break; default: - // Metadata, tile-list, and reserved OBUs do not contribute to this still-image reconstruction - // pass. Their declared payload has already been skipped by the parent reader. the reference decoder rejects a - // nonempty unrecognized payload that contains only zeros because it has no trailing one bit. + // Reserved OBUs do not contribute to this still-image reconstruction pass. Their declared payload has + // already been skipped by the parent reader. The reference decoder rejects a nonempty unrecognized + // payload that contains only zeros because it has no trailing one bit. if (payloadSize > 0) { 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 // state. Any decoder failure leaves the preceding session snapshot intact for deterministic cleanup. - this.decoder!.CompleteFrame(); - this.frameReferenceState.CompleteFrame(this.FrameHeader!, this.SequenceHeader!.IsFrameIdNumbersPresent); - this.decoder = null; + decoderToComplete.CompleteFrame(); + this.frameReferenceState.CompleteFrame( + this.CurrentFrameHeader, + this.CurrentSequenceHeader.IsFrameIdNumbersPresent); + + activeDecoder = null; seenFrameHeader = false; nextTileStart = 0; 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."); } @@ -450,9 +540,9 @@ internal class ObuReader /// public void Reset() { - this.decoder = null; this.SequenceHeader = null; this.FrameHeader = null; + this.ResetMetadata(); this.currentOperatingPointIdc = 0; this.frameReferenceState.Reset(); this.referenceFrames?.Reset(); @@ -570,13 +660,234 @@ internal class ObuReader } } + /// + /// Reads and validates one AV1 metadata OBU payload. + /// + /// The bounded metadata payload. + /// The number of payload bytes occupied by decoded syntax. + private int ReadMetadata(Span payload) + { + int metadataOffset = 0; + ulong metadataTypeValue = ReadMetadataType(payload, ref metadataOffset); + Span 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); + } + + /// + /// Reads the unsigned little-endian base-128 metadata type. + /// + /// The bounded metadata payload. + /// The byte offset, advanced past the metadata type. + /// The decoded metadata type. + private static ulong ReadMetadataType(ReadOnlySpan 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."); + } + + /// + /// Validates byte-aligned ITU-T T.35 metadata syntax and trailing bits. + /// + /// The metadata bytes following the metadata type. + private static void ValidateItutT35Metadata(ReadOnlySpan 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."); + } + } + + /// + /// Reads scalability metadata syntax so its bounded payload and trailing bits can be validated. + /// + /// The metadata payload reader. + 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); + } + } + } + } + + /// + /// Reads timecode metadata syntax so its bounded payload and trailing bits can be validated. + /// + /// The metadata payload reader. + 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); + } + } + + /// + /// Finds the final nonzero byte in one bounded payload. + /// + /// The payload to inspect. + /// The final nonzero byte index, or -1 when every byte is zero. + private static int FindLastNonzeroByteIndex(ReadOnlySpan payload) + { + for (int i = payload.Length - 1; i >= 0; i--) + { + if (payload[i] != 0) + { + return i; + } + } + + return -1; + } + /// /// Computes the mode-information dimensions and stride for the current frame. /// /// The sequence header defining the maximum frame geometry and superblock size. private void ComputeImageSize(ObuSequenceHeader sequenceHeader) { - ObuFrameHeader frameHeader = this.FrameHeader!; + ObuFrameHeader frameHeader = this.CurrentFrameHeader; frameHeader.ModeInfoColumnCount = 2 * ((frameHeader.FrameSize.FrameWidth + 7) >> 3); frameHeader.ModeInfoRowCount = 2 * ((frameHeader.FrameSize.FrameHeight + 7) >> 3); frameHeader.ModeInfoStride = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, Av1Constants.MaxSuperBlockSizeLog2) >> Av1Constants.ModeInfoSizeLog2; @@ -609,7 +920,6 @@ internal class ObuReader sequenceHeader.TimingInfo = null; sequenceHeader.DecoderModelInfoPresentFlag = false; sequenceHeader.InitialDisplayDelayPresentFlag = false; - sequenceHeader.OperatingPoint = new ObuOperatingPoint[1]; ObuOperatingPoint operatingPoint = new(); sequenceHeader.OperatingPoint[0] = operatingPoint; operatingPoint.OperatorIndex = 0; @@ -641,8 +951,12 @@ internal class ObuReader } sequenceHeader.InitialDisplayDelayPresentFlag = reader.ReadBoolean(); - uint operatingPointsCnt = reader.ReadLiteral(5) + 1; - sequenceHeader.OperatingPoint = new ObuOperatingPoint[operatingPointsCnt]; + int operatingPointsCnt = (int)reader.ReadLiteral(5) + 1; + if (sequenceHeader.OperatingPoint.Length != operatingPointsCnt) + { + sequenceHeader.OperatingPoint = new ObuOperatingPoint[operatingPointsCnt]; + } + for (int i = 0; i < operatingPointsCnt; i++) { sequenceHeader.OperatingPoint[i] = new ObuOperatingPoint @@ -671,7 +985,8 @@ internal class ObuReader { // Operating-point delays affect scheduling rather than still-image reconstruction, but their // 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 @@ -991,8 +1306,8 @@ internal class ObuReader /// The reader positioned at the super-resolution syntax. private void ReadSuperResolutionParameters(ref Av1BitStreamReader reader) { - ObuSequenceHeader sequenceHeader = this.SequenceHeader!; - ObuFrameHeader frameHeader = this.FrameHeader!; + ObuSequenceHeader sequenceHeader = this.CurrentSequenceHeader; + ObuFrameHeader frameHeader = this.CurrentFrameHeader; bool useSuperResolution = false; if (sequenceHeader.EnableSuperResolution) @@ -1034,7 +1349,7 @@ internal class ObuReader /// The reader positioned at the render-size syntax. private void ReadRenderSize(ref Av1BitStreamReader reader) { - ObuFrameHeader frameHeader = this.FrameHeader!; + ObuFrameHeader frameHeader = this.CurrentFrameHeader; bool renderSizeAndFrameSizeDifferent = reader.ReadBoolean(); if (renderSizeAndFrameSizeDifferent) @@ -1058,8 +1373,8 @@ internal class ObuReader /// A value indicating whether dimensions are signaled instead of inherited from the sequence maximum. private void ReadFrameSize(ref Av1BitStreamReader reader, bool frameSizeOverrideFlag) { - ObuSequenceHeader sequenceHeader = this.SequenceHeader!; - ObuFrameHeader frameHeader = this.FrameHeader!; + ObuSequenceHeader sequenceHeader = this.CurrentSequenceHeader; + ObuFrameHeader frameHeader = this.CurrentFrameHeader; if (frameSizeOverrideFlag) { @@ -1092,8 +1407,8 @@ internal class ObuReader /// The retained reconstructed frames selected by the current reference mapping. private void ReadFrameSizeWithReferences(ref Av1BitStreamReader reader, Av1ReferenceFrameStore referenceFrames) { - ObuSequenceHeader sequenceHeader = this.SequenceHeader!; - ObuFrameHeader frameHeader = this.FrameHeader!; + ObuSequenceHeader sequenceHeader = this.CurrentSequenceHeader; + ObuFrameHeader frameHeader = this.CurrentFrameHeader; ObuFrameSize frameSize = frameHeader.FrameSize; Span referenceFrameIndices = frameHeader.GetReferenceFrameIndices(); bool foundReference = false; @@ -1107,7 +1422,7 @@ internal class ObuReader continue; } - Av1ReferenceFrame referenceFrame = referenceFrames.Resolve((int)referenceFrameIndices[reference])!; + Av1ReferenceFrame referenceFrame = referenceFrames.ResolveRequired((int)referenceFrameIndices[reference]); ObuFrameSize referenceSize = referenceFrame.FrameHeader.FrameSize; // 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++) { - Av1ReferenceFrame referenceFrame = referenceFrames.Resolve((int)referenceFrameIndices[reference])!; + Av1ReferenceFrame referenceFrame = referenceFrames.ResolveRequired((int)referenceFrameIndices[reference]); int referenceWidth = referenceFrame.FrameBuffer.Width; int referenceHeight = referenceFrame.FrameBuffer.Height; @@ -1210,6 +1525,9 @@ internal class ObuReader tileInfo.MaxLog2TileRowCount = (int)Av1Math.CeilLog2((uint)Math.Min(superblockRowCount, Av1Constants.MaxTileRowCount)); tileInfo.MinLog2TileCount = Math.Max(tileInfo.MinLog2TileColumnCount, TileLog2(maxTileAreaOfSuperBlock, superblockColumnCount * superblockRowCount)); 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) { tileInfo.TileColumnCountLog2 = tileInfo.MinLog2TileColumnCount; @@ -1228,7 +1546,6 @@ internal class ObuReader int tileWidthSuperblock = Av1Math.DivideLog2Ceiling(superblockColumnCount, tileInfo.TileColumnCountLog2); DebugGuard.MustBeLessThanOrEqualTo(tileWidthSuperblock, tileInfo.MaxTileWidthSuperblock, nameof(tileWidthSuperblock)); int i = 0; - tileInfo.TileColumnStartModeInfo = new int[superblockColumnCount + 1]; for (int startSuperblock = 0; startSuperblock < superblockColumnCount; startSuperblock += tileWidthSuperblock) { tileInfo.TileColumnStartModeInfo[i] = startSuperblock << superblockShift; @@ -1255,7 +1572,6 @@ internal class ObuReader int tileHeightSuperblock = Av1Math.DivideLog2Ceiling(superblockRowCount, tileInfo.TileRowCountLog2); DebugGuard.MustBeLessThanOrEqualTo(tileHeightSuperblock, tileInfo.MaxTileHeightSuperblock, nameof(tileHeightSuperblock)); i = 0; - tileInfo.TileRowStartModeInfo = new int[superblockRowCount + 1]; for (int startSuperblock = 0; startSuperblock < superblockRowCount; startSuperblock += tileHeightSuperblock) { tileInfo.TileRowStartModeInfo[i] = startSuperblock << superblockShift; @@ -1272,6 +1588,11 @@ internal class ObuReader int i = 0; 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; uint maxWidth = (uint)Math.Min(superblockColumnCount - startSuperBlock, tileInfo.MaxTileWidthSuperblock); uint widthInSuperBlocks = reader.ReadNonSymmetric(maxWidth) + 1; @@ -1302,6 +1623,11 @@ internal class ObuReader startSuperBlock = 0; 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; uint maxHeight = (uint)Math.Min(superblockRowCount - startSuperBlock, tileInfo.MaxTileHeightSuperblock); uint heightInSuperBlocks = reader.ReadNonSymmetric(maxHeight) + 1; @@ -1366,8 +1692,8 @@ internal class ObuReader /// The OBU header identifying the frame's temporal and spatial layers. private void ReadUncompressedFrameHeader(ref Av1BitStreamReader reader, ObuHeader header) { - ObuSequenceHeader sequenceHeader = this.SequenceHeader!; - ObuFrameHeader frameHeader = this.FrameHeader!; + ObuSequenceHeader sequenceHeader = this.CurrentSequenceHeader; + ObuFrameHeader frameHeader = this.CurrentFrameHeader; Av1ReferenceFrame? primaryReference = null; bool frameSizeOverrideFlag = false; int idLength = sequenceHeader.FrameIdLength; @@ -1395,7 +1721,8 @@ internal class ObuReader if (sequenceHeader.DecoderModelInfoPresentFlag && sequenceHeader.TimingInfo?.EqualPictureInterval == false) { // 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) @@ -1464,7 +1791,8 @@ internal class ObuReader if (frameHeader.ShowFrame && sequenceHeader.DecoderModelInfoPresentFlag && sequenceHeader.TimingInfo?.EqualPictureInterval == false) { // 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) @@ -1579,7 +1907,8 @@ internal class ObuReader bool bufferRemovalTimePresent = reader.ReadBoolean(); if (bufferRemovalTimePresent) { - int bufferRemovalTimeLength = (int)sequenceHeader.DecoderModelInfo!.BufferRemovalTimeLength; + ObuDecoderModelInfo decoderModelInfo = sequenceHeader.GetDecoderModelInfo(); + int bufferRemovalTimeLength = (int)decoderModelInfo.BufferRemovalTimeLength; foreach (ObuOperatingPoint operatingPoint in sequenceHeader.OperatingPoint) { // 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 // 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) @@ -1700,7 +2029,8 @@ internal class ObuReader // SetupFrameBufferReferences(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) { 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 // 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.ModeDeltas.AsSpan().CopyTo(frameHeader.LoopFilterParameters.ModeDeltas); + primaryReference.FrameHeader.LoopFilterParameters.ReferenceDeltas.CopyTo(frameHeader.LoopFilterParameters.ReferenceDeltas); + primaryReference.FrameHeader.LoopFilterParameters.ModeDeltas.CopyTo(frameHeader.LoopFilterParameters.ModeDeltas); } // 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. - // 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); ReadQuantizationParameters(ref reader, sequenceHeader, frameHeader); ReadSegmentationParameters(ref reader, frameHeader, primaryReference?.FrameHeader.SegmentationParameters); @@ -1842,16 +2173,6 @@ internal class ObuReader } } - /// - /// Determines whether segmentation and a specific per-segment feature are both enabled. - /// - /// The frame segmentation state. - /// The segment identifier. - /// The feature to inspect. - /// when the feature is active; otherwise, . - private static bool IsSegmentationFeatureActive(ObuSegmentationParameters segmentationParameters, int segmentId, ObuSegmentationLevelFeature feature) - => segmentationParameters.Enabled && segmentationParameters.IsFeatureActive(segmentId, feature); - /// /// Reads an AV1 frame header and removes its byte length from the remaining OBU payload size. /// @@ -1889,9 +2210,9 @@ internal class ObuReader ref int nextTileStart, out bool isLastTileGroup) { - ObuSequenceHeader sequenceHeader = this.SequenceHeader!; - ObuFrameHeader frameHeader = this.FrameHeader!; - ObuTileGroupHeader tileInfo = this.FrameHeader!.TilesInfo; + ObuSequenceHeader sequenceHeader = this.CurrentSequenceHeader; + ObuFrameHeader frameHeader = this.CurrentFrameHeader; + ObuTileGroupHeader tileInfo = frameHeader.TilesInfo; int tileCount = tileInfo.TileColumnCount * tileInfo.TileRowCount; int startBitPosition = reader.BitPosition; bool tileStartAndEndPresentFlag = false; @@ -1982,8 +2303,6 @@ internal class ObuReader { return; } - - // TODO: Share doCdef and doLoopRestoration } /// @@ -2153,7 +2472,7 @@ internal class ObuReader { int featureValue = 0; bool featureEnabled = reader.ReadBoolean(); - frameHeader.SegmentationParameters.FeatureEnabled[i, j] = featureEnabled; + frameHeader.SegmentationParameters.SetFeatureEnabled(i, j, featureEnabled); int clippedValue = 0; 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. // 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 @@ -2188,8 +2512,8 @@ internal class ObuReader { for (int j = 0; j < Av1Constants.SegmentationLevelMax; j++) { - frameHeader.SegmentationParameters.FeatureEnabled[i, j] = false; - frameHeader.SegmentationParameters.FeatureData[i, j] = 0; + frameHeader.SegmentationParameters.SetFeatureEnabled(i, j, false); + frameHeader.SegmentationParameters.SetFeatureData(i, j, 0); } } } @@ -2200,7 +2524,7 @@ internal class ObuReader { 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; if (j >= (int)ObuSegmentationLevelFeature.ReferenceFrame) @@ -2219,7 +2543,7 @@ internal class ObuReader /// The sequence header defining the active color planes. private void ReadLoopFilterParameters(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader) { - ObuFrameHeader frameHeader = this.FrameHeader!; + ObuFrameHeader frameHeader = this.CurrentFrameHeader; if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy) { return; @@ -2400,7 +2724,10 @@ internal class ObuReader { // 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. - 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++) @@ -2764,4 +3091,20 @@ internal class ObuReader 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 creator) : ITileReaderProvider + { + private readonly Func creator = creator; + + public IAv1TileReader Get() => this.creator(); + } + + private readonly struct FixedTileReaderProvider(IAv1TileReader tileReader) : ITileReaderProvider + { + private readonly IAv1TileReader tileReader = tileReader; + + public IAv1TileReader Get() => this.tileReader; + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSegmentationParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSegmentationParameters.cs index 55fbf43fa..eda7e99e2 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSegmentationParameters.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSegmentationParameters.cs @@ -6,27 +6,32 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Contains the AV1 segmentation state and per-segment feature values for a frame. /// -internal class ObuSegmentationParameters +internal sealed class ObuSegmentationParameters { /// - /// 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. /// - public int[][] QMLevel { get; set; } = new int[3][]; + private InlineArray8> featureEnabled; /// - /// 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. /// - public bool[,] FeatureEnabled { get; set; } = new bool[Av1Constants.MaxSegmentCount, Av1Constants.SegmentationLevelMax]; + private InlineArray8> featureData; /// - /// 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. /// - public bool Enabled { get; set; } + private InlineArray4> qmLevel; + + /// + /// Gets the mutable effective quantization-matrix levels for the Y, U, and V planes. + /// + public Span> QMLevel => this.qmLevel[..Av1Constants.MaxPlanes]; /// - /// Gets or sets the value of every feature for every segment. + /// Gets or sets a value indicating whether segmentation is enabled for the frame. /// - public int[,] FeatureData { get; set; } = new int[Av1Constants.MaxSegmentCount, Av1Constants.SegmentationLevelMax]; + public bool Enabled { get; set; } /// /// Gets or sets a value indicating whether segment identifiers are decoded before skip-mode decisions. @@ -60,7 +65,33 @@ internal class ObuSegmentationParameters /// The feature to inspect. /// when the feature is active; otherwise, . public bool IsFeatureActive(int segmentId, ObuSegmentationLevelFeature feature) - => this.FeatureEnabled[segmentId, (int)feature]; + => this.featureEnabled[segmentId][(int)feature]; + + /// + /// Gets a feature value for a segment. + /// + /// The segment identifier. + /// The feature identifier. + /// The stored feature value. + public int GetFeatureData(int segmentId, int featureId) => this.featureData[segmentId][featureId]; + + /// + /// Sets whether a feature is active for a segment. + /// + /// The segment identifier. + /// The feature identifier. + /// Whether the feature is active. + public void SetFeatureEnabled(int segmentId, int featureId, bool enabled) + => this.featureEnabled[segmentId][featureId] = enabled; + + /// + /// Sets a feature value for a segment. + /// + /// The segment identifier. + /// The feature identifier. + /// The feature value. + public void SetFeatureData(int segmentId, int featureId, int value) + => this.featureData[segmentId][featureId] = value; /// /// 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++) { - this.FeatureEnabled[segment, feature] = source.FeatureEnabled[segment, feature]; - this.FeatureData[segment, feature] = source.FeatureData[segment, feature]; + this.featureEnabled[segment][feature] = source.featureEnabled[segment][feature]; + this.featureData[segment][feature] = source.featureData[segment][feature]; } } } diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceHeader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceHeader.cs index c1b522b41..167cebbf3 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceHeader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceHeader.cs @@ -6,13 +6,18 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Stores AV1 sequence capabilities, dimensions, timing, and color configuration. /// -internal class ObuSequenceHeader +internal sealed class ObuSequenceHeader { /// /// Backs while its dependent geometry is updated. /// private bool use128x128Superblock; + /// + /// Initializes a new instance of the class with the normative 64x64 superblock geometry. + /// + public ObuSequenceHeader() => this.Use128x128Superblock = false; + /// /// Gets or sets a value indicating whether filter-intra prediction is enabled. /// @@ -213,4 +218,12 @@ internal class ObuSequenceHeader /// Gets or sets the additional frame-identifier bit count signaled by the sequence header. /// public uint AdditionalFrameIdLength { get; set; } + + /// + /// Gets the decoder-buffer model information required by syntax whose presence flag is set. + /// + /// The decoder-buffer model information. + public ObuDecoderModelInfo GetDecoderModelInfo() => + this.DecoderModelInfo + ?? throw new InvalidOperationException("The AV1 sequence has no decoder-model information."); } diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSkipModeParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSkipModeParameters.cs index a45b504ea..e5c9766d2 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSkipModeParameters.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSkipModeParameters.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Contains the availability and enabled state of AV1 skip mode for a frame. /// -internal class ObuSkipModeParameters +internal sealed class ObuSkipModeParameters { /// /// Gets a value indicating whether the frame is permitted to use skip mode. diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTileGroupHeader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTileGroupHeader.cs index 5f04768e0..b508b1038 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTileGroupHeader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTileGroupHeader.cs @@ -1,13 +1,18 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Contains the tile layout derived from an AV1 frame header. /// -internal class ObuTileGroupHeader +internal sealed class ObuTileGroupHeader { + private InlineTileColumnBoundaryArray tileColumnStartModeInfo; + private InlineTileRowBoundaryArray tileRowStartModeInfo; + /// /// Gets or sets the maximum tile width, in superblocks. /// @@ -54,9 +59,9 @@ internal class ObuTileGroupHeader public int TileColumnCount { get; set; } /// - /// Gets or sets the starting superblock column for each tile column. + /// Gets the fixed-capacity starting superblock column storage for each tile column. /// - public int[] TileColumnStartModeInfo { get; set; } = new int[Av1Constants.MaxTileRowCount + 1]; + public Span TileColumnStartModeInfo => this.tileColumnStartModeInfo; /// /// 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; } /// - /// Gets or sets the starting superblock row for each tile row. + /// Gets the fixed-capacity starting superblock row storage for each tile row. /// - public int[] TileRowStartModeInfo { get; set; } = new int[Av1Constants.MaxTileColumnCount + 1]; + public Span TileRowStartModeInfo => this.tileRowStartModeInfo; /// /// 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. /// 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; + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTimingInfo.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTimingInfo.cs index 370cbf4d6..8cbb49487 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTimingInfo.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTimingInfo.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; /// /// Contains the timing syntax signaled by an AV1 sequence header. /// -internal class ObuTimingInfo +internal sealed class ObuTimingInfo { /// /// Gets or sets NumUnitsInDisplayTick. NumUnitsInDisplayTick is the number of time units of a clock operating at the frequency TimeScale Hz that diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs index a3d1d7fd7..a592685ac 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs @@ -629,7 +629,7 @@ internal class ObuWriter { for (int featureId = 0; featureId < Av1Constants.SegmentationLevelMax; featureId++) { - bool enabled = segmentation.FeatureEnabled[segmentId, featureId]; + bool enabled = segmentation.IsFeatureActive(segmentId, (ObuSegmentationLevelFeature)featureId); writer.WriteBoolean(enabled); if (!enabled) { @@ -637,7 +637,7 @@ internal class ObuWriter } int bitCount = Av1Constants.SegmentationFeatureBits[featureId]; - int value = segmentation.FeatureData[segmentId, featureId]; + int value = segmentation.GetFeatureData(segmentId, featureId); if (Av1Constants.SegmentationFeatureSigned[featureId] == 1) { writer.WriteSignedFromUnsigned(value, bitCount + 1); diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs index 279e5c5e7..64e200a00 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs +++ b/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.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; @@ -41,7 +42,7 @@ internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable /// /// The retained reconstructed frames addressable by inter prediction. /// - private readonly Av1ReferenceFrameStore? referenceFrames; + private readonly Av1ReferenceFrameStore referenceFrames; /// /// The coefficient inverse-quantization stage shared across superblocks. @@ -70,15 +71,15 @@ internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable /// The parsed AV1 frame header. /// The parsed superblock and block-mode information. /// The destination planar sample buffers. - /// - /// The retained reconstructed frames selected by inter blocks, or for intra-only reconstruction. - /// + /// The retained reconstructed frames selected by inter blocks. + /// The complete decoder-session palette map state. public Av1FrameDecoder( ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, Av1FrameInfo frameInfo, Av1FrameBuffer frameBuffer, - Av1ReferenceFrameStore? referenceFrames = null) + Av1ReferenceFrameStore referenceFrames, + Av1TileReader.PaletteColorIndexMaps? paletteColorIndexMaps = null) { this.sequenceHeader = sequenceHeader; this.frameHeader = frameHeader; @@ -87,20 +88,33 @@ internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable this.referenceFrames = referenceFrames; this.inverseQuantizer = new(sequenceHeader, frameHeader); this.deQuants = new(sequenceHeader, frameHeader); - this.loopFilterContext = new(sequenceHeader); - this.blockDecoder = new( - this.sequenceHeader, - this.frameHeader, - this.frameBuffer, - this.loopFilterContext, - this.inverseQuantizer, - this.referenceFrames); + this.loopFilterContext = new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); + try + { + this.blockDecoder = new( + this.sequenceHeader, + this.frameHeader, + this.frameBuffer, + this.loopFilterContext, + this.inverseQuantizer, + this.referenceFrames, + paletteColorIndexMaps); + } + catch + { + this.loopFilterContext.Dispose(); + throw; + } } /// /// Releases the pooled block-reconstruction workspaces owned by this decoder. /// - public void Dispose() => this.blockDecoder.Dispose(); + public void Dispose() + { + this.blockDecoder.Dispose(); + this.loopFilterContext.Dispose(); + } /// /// Applies the in-loop frame stages after every superblock has been reconstructed. diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs index 63d09979a..0fe3b72db 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef; /// /// Applies AV1 constrained directional enhancement filtering to a reconstructed still-image frame. /// -internal class Av1CdefDecoder +internal sealed class Av1CdefDecoder { /// /// The width and height of a CDEF unit in 4x4 luma mode-information units. @@ -30,6 +30,21 @@ internal class Av1CdefDecoder /// private const int MaximumBlocksPerUnit = 8 * 8; + /// + /// The maximum width or height of one CDEF unit in plane samples. + /// + private const int MaximumUnitPlaneSize = CdefUnitModeInfoSize << Av1Constants.ModeInfoSizeLog2; + + /// + /// The stride of the reusable bordered CDEF source unit. + /// + private const int SourceStride = MaximumUnitPlaneSize + (SourceBorder * 2); + + /// + /// The sample count of the reusable bordered CDEF source unit. + /// + private const int SourceBufferLength = SourceStride * (MaximumUnitPlaneSize + (SourceBorder * 2)); + /// /// The sequence-level superblock, bit-depth, and color configuration. /// @@ -97,144 +112,109 @@ internal class Av1CdefDecoder return; } - int lumaBlockColumnCount = this.frameHeader.ModeInfoColumnCount >> 1; - int lumaBlockRowCount = this.frameHeader.ModeInfoRowCount >> 1; - int mapLength = lumaBlockColumnCount * lumaBlockRowCount; - MemoryAllocator allocator = this.frameBuffer.MemoryAllocator; - using IMemoryOwner directionOwner = allocator.Allocate(mapLength, AllocationOptions.Clean); - using IMemoryOwner varianceOwner = allocator.Allocate(mapLength, AllocationOptions.Clean); - Span directions = directionOwner.Memory.Span[..mapLength]; - Span variances = varianceOwner.Memory.Span[..mapLength]; ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig; - - // Luma must be processed first even when its strengths are zero because chroma CDEF - // consumes directions derived from the immutable, deblocked luma source. - for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++) + int planeCount = colorConfig.PlaneCount; + Span subsamplingX = stackalloc int[3]; + Span subsamplingY = stackalloc int[3]; + Span planeWidths = stackalloc int[3]; + Span lineBufferOffsets = stackalloc int[3]; + Span columnBufferOffsets = stackalloc int[3]; + Span columnBufferLengths = stackalloc int[3]; + int lineBufferLength = 0; + int columnBufferLength = 0; + + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { Av1Plane plane = (Av1Plane)planeIndex; - int subsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0; - int subsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0; - this.FilterPlane(plane, subsamplingX, subsamplingY, directions, variances, lumaBlockColumnCount); - } - } - - /// - /// Filters one color plane from an immutable snapshot of its deblocked samples. - /// - /// The color plane to filter. - /// The horizontal chroma subsampling shift. - /// The vertical chroma subsampling shift. - /// The frame-wide luma direction map in 8x8 block order. - /// The frame-wide luma directional-variance map in 8x8 block order. - /// The number of 8x8 blocks in an aligned luma row. - private void FilterPlane( - Av1Plane plane, - int subsamplingX, - int subsamplingY, - Span directions, - Span 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 sourceOwner = this.frameBuffer.MemoryAllocator.Allocate(sourceLength); - Span 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 lowBitDepthDestination = default; - Span highBitDepthDestination = default; - int destinationStride; - if (this.frameBuffer.BytesPerSample == 2) - { - Span signedDestination = this.frameBuffer.DeriveBlockPointer16( - plane, - Point.Empty, - subsamplingX, - subsamplingY, - out destinationStride); - - highBitDepthDestination = MemoryMarshal.Cast(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); + int planeSubsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0; + int planeSubsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0; + int planeWidth = this.frameHeader.ModeInfoColumnCount << (Av1Constants.ModeInfoSizeLog2 - planeSubsamplingX); + int maximumUnitHeight = MaximumUnitPlaneSize >> planeSubsamplingY; + + subsamplingX[planeIndex] = planeSubsamplingX; + subsamplingY[planeIndex] = planeSubsamplingY; + planeWidths[planeIndex] = planeWidth; + lineBufferOffsets[planeIndex] = lineBufferLength; + columnBufferOffsets[planeIndex] = columnBufferLength; + columnBufferLengths[planeIndex] = (maximumUnitHeight + (SourceBorder * 2)) * SourceBorder; + lineBufferLength += planeWidth * SourceBorder * 2; + columnBufferLength += columnBufferLengths[planeIndex]; } - ObuConstraintDirectionalEnhancementFilterParameters parameters = this.frameHeader.CdefParameters; - int coefficientShift = Math.Max(this.frameBuffer.BitDepth.GetBitCount() - 8, 0); - int blockWidth = 8 >> subsamplingX; - int blockHeight = 8 >> subsamplingY; + int scratchLength = SourceBufferLength + lineBufferLength + columnBufferLength; + MemoryAllocator allocator = this.frameBuffer.MemoryAllocator; + using IMemoryOwner scratchOwner = allocator.Allocate(scratchLength); + Span scratch = scratchOwner.Memory.Span[..scratchLength]; + Span source = scratch[..SourceBufferLength]; + Span lineBuffer = scratch.Slice(SourceBufferLength, lineBufferLength); + Span columnBuffer = scratch[(SourceBufferLength + lineBufferLength)..]; + Span directions = stackalloc int[MaximumBlocksPerUnit]; + Span variances = stackalloc int[MaximumBlocksPerUnit]; + Span cdefLeft = stackalloc bool[3]; int unitColumnCount = (this.frameHeader.ModeInfoColumnCount + CdefUnitModeInfoSize - 1) / CdefUnitModeInfoSize; int unitRowCount = (this.frameHeader.ModeInfoRowCount + CdefUnitModeInfoSize - 1) / CdefUnitModeInfoSize; + // libaom traverses one 64x64 unit at a time so chroma consumes the luma directions before + // the fixed direction arrays are reused. This also bounds direction storage to 64 entries. for (int unitRow = 0; unitRow < unitRowCount; unitRow++) { + cdefLeft.Clear(); + int unitModeInfoRow = unitRow * CdefUnitModeInfoSize; + + // Preserve the final two unfiltered rows before this unit row is modified. The alternating + // slots keep the previous row available while the next row's top border is captured. + if (unitRow < unitRowCount - 1) + { + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) + { + Av1Plane plane = (Av1Plane)planeIndex; + int planeSubsamplingX = subsamplingX[planeIndex]; + int planeSubsamplingY = subsamplingY[planeIndex]; + int planeWidth = planeWidths[planeIndex]; + int nextPlaneRow = ((unitModeInfoRow + CdefUnitModeInfoSize) << Av1Constants.ModeInfoSizeLog2) >> planeSubsamplingY; + int lineSlotOffset = lineBufferOffsets[planeIndex] + + ((unitRow & 1) * SourceBorder * planeWidth); + + this.GetPlaneDestination( + plane, + planeSubsamplingX, + planeSubsamplingY, + out Span lowBitDepthDestination, + out Span highBitDepthDestination, + out int destinationStride); + + this.CopyFrameRectangle( + lowBitDepthDestination, + highBitDepthDestination, + destinationStride + ((nextPlaneRow - SourceBorder) * destinationStride), + destinationStride, + lineBuffer, + lineSlotOffset, + planeWidth, + planeWidth, + SourceBorder); + } + } + for (int unitColumn = 0; unitColumn < unitColumnCount; unitColumn++) { - int unitModeInfoRow = unitRow * CdefUnitModeInfoSize; int unitModeInfoColumn = unitColumn * CdefUnitModeInfoSize; int strengthIndex = this.GetStrengthIndex(unitModeInfoColumn, unitModeInfoRow); if (strengthIndex < 0) { + cdefLeft.Clear(); continue; } int yStrength = parameters.YStrength[strengthIndex]; int uvStrength = parameters.UvStrength[strengthIndex]; - bool unitNeedsDirections = yStrength != 0 || - (this.sequenceHeader.ColorConfig.PlaneCount > 1 && uvStrength != 0); - - if ((plane == Av1Plane.Y && !unitNeedsDirections) || (plane != Av1Plane.Y && uvStrength == 0)) + bool unitNeedsDirections = yStrength != 0 || (planeCount > 1 && uvStrength != 0); + if (!unitNeedsDirections) { + cdefLeft.Clear(); 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 unitModeInfoColumnEnd = Math.Min(unitModeInfoColumn + CdefUnitModeInfoSize, this.frameHeader.ModeInfoColumnCount); 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 - // without allocating a managed block list or repeating four skip-map lookups during filtering. - for (; blockIndex < blockCount - 1; blockIndex += 2) + // Luma is always prepared first when either plane type needs CDEF because it owns + // the direction search. Chroma then reuses those per-unit results without a frame map. + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) + { + if (planeIndex != (int)Av1Plane.Y && uvStrength == 0) { - CdefBlock firstBlock = blocks[blockIndex]; - CdefBlock secondBlock = blocks[blockIndex + 1]; - 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]); + cdefLeft[planeIndex] = false; + continue; } - if (blockIndex < blockCount) - { - CdefBlock block = blocks[blockIndex]; - int directionIndex = block.GetDirectionIndex(lumaBlockColumnCount); - - directions[directionIndex] = Av1CdefFilter.FindDirection( - source, - block.GetSourceOffset(sourceStride, SourceBorder), - sourceStride, - coefficientShift, - out variances[directionIndex]); - } + int planeWidth = planeWidths[planeIndex]; + int currentLineSlotOffset = lineBufferOffsets[planeIndex] + + (((unitRow - 1) & 1) * SourceBorder * planeWidth); + ReadOnlySpan topLineBuffer = unitRow == 0 + ? default + : lineBuffer.Slice(currentLineSlotOffset, SourceBorder * planeWidth); + Span planeColumnBuffer = columnBuffer.Slice( + columnBufferOffsets[planeIndex], + columnBufferLengths[planeIndex]); + + this.FilterPlane( + (Av1Plane)planeIndex, + subsamplingX[planeIndex], + subsamplingY[planeIndex], + unitModeInfoColumn, + unitModeInfoRow, + ref blocks, + blockCount, + directions, + variances, + yStrength, + uvStrength, + source, + topLineBuffer, + planeColumnBuffer, + cdefLeft[planeIndex]); + + cdefLeft[planeIndex] = true; } + } + } + } - for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) - { - CdefBlock block = blocks[blockIndex]; - int directionIndex = block.GetDirectionIndex(lumaBlockColumnCount); + /// + /// Filters one color plane in a CDEF unit from a bounded immutable source snapshot. + /// + /// The color plane to filter. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// The unit's frame-relative column in 4x4 luma units. + /// The unit's frame-relative row in 4x4 luma units. + /// The unit's non-skipped 8x8 luma blocks. + /// The number of initialized entries in . + /// The unit-local luma directions in block-list order. + /// The unit-local luma directional variances in block-list order. + /// The coded luma strength. + /// The coded chroma strength. + /// The reusable bordered source-unit buffer. + /// The two preserved unfiltered rows immediately above this unit row. + /// The preserved unfiltered columns immediately left of this unit. + /// Whether the preceding unit overwrote samples needed by this unit. + private void FilterPlane( + Av1Plane plane, + int subsamplingX, + int subsamplingY, + int unitModeInfoColumn, + int unitModeInfoRow, + ref CdefBlockList blocks, + int blockCount, + Span directions, + Span variances, + int yStrength, + int uvStrength, + Span source, + ReadOnlySpan topLineBuffer, + Span 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) - { - continue; - } + this.GetPlaneDestination( + plane, + subsamplingX, + subsamplingY, + out Span lowBitDepthDestination, + out Span highBitDepthDestination, + out int destinationStride); - int filteredPrimaryStrength = plane == Av1Plane.Y - ? Av1CdefFilter.AdjustStrength(primaryStrength, variances[directionIndex]) - : primaryStrength; + if (hasTop) + { + Av1CdefFilter.CopyPlane( + topLineBuffer, + copyColumn, + planeWidth, + source, + sourceColumn, + SourceStride, + copyWidth, + SourceBorder); + } - if (filteredPrimaryStrength == 0 && secondaryStrength == 0) - { - continue; - } + this.CopyFrameRectangle( + lowBitDepthDestination, + highBitDepthDestination, + destinationStride + (planeRow * destinationStride) + copyColumn, + destinationStride, + source, + (SourceBorder * SourceStride) + sourceColumn, + SourceStride, + copyWidth, + unitHeight); + + if (hasBottom) + { + this.CopyFrameRectangle( + lowBitDepthDestination, + highBitDepthDestination, + destinationStride + ((planeRow + unitHeight) * destinationStride) + copyColumn, + destinationStride, + source, + ((SourceBorder + unitHeight) * SourceStride) + sourceColumn, + SourceStride, + copyWidth, + SourceBorder); + } - // 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[directionIndex], subsamplingX, subsamplingY) - : 0; - int planeColumn = (block.ModeInfoColumn << Av1Constants.ModeInfoSizeLog2) >> subsamplingX; - int planeRow = (block.ModeInfoRow << Av1Constants.ModeInfoSizeLog2) >> subsamplingY; - int blockSourceOffset = ((planeRow + SourceBorder) * sourceStride) + planeColumn + SourceBorder; - int blockDestinationOffset = destinationStride + (planeRow * destinationStride) + planeColumn; - - 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); - } - } + int preservedHeight = SourceBorder + unitHeight + (hasBottom ? SourceBorder : 0); + if (leftPrepared) + { + Av1CdefFilter.CopyPlane( + columnBuffer, + 0, + SourceBorder, + source, + 0, + SourceStride, + SourceBorder, + preservedHeight); + } + + // Save the final unfiltered columns before this unit writes its destination. The next unit + // restores them over the frame samples that this unit has already replaced. + Av1CdefFilter.CopyPlane( + source, + unitWidth, + SourceStride, + columnBuffer, + 0, + SourceBorder, + SourceBorder, + preservedHeight); + + ObuConstraintDirectionalEnhancementFilterParameters parameters = this.frameHeader.CdefParameters; + int coefficientShift = Math.Max(this.frameBuffer.BitDepth.GetBitCount() - 8, 0); + int blockWidth = 8 >> subsamplingX; + int blockHeight = 8 >> subsamplingY; + int codedStrength = plane == Av1Plane.Y ? yStrength : uvStrength; + int primaryStrength = (codedStrength / 4) << coefficientShift; + int secondaryStrength = codedStrength % 4; + + // The two-bit secondary field leaves value three unused and represents strength four instead. + secondaryStrength += secondaryStrength == 3 ? 1 : 0; + secondaryStrength <<= coefficientShift; + int damping = parameters.Damping + coefficientShift - (plane == Av1Plane.Y ? 0 : 1); + + if (plane == Av1Plane.Y) + { + int blockIndex = 0; + + // The reference decoder analyzes two listed 8x8 blocks together. The per-unit fixed list preserves that traversal + // without allocating a managed block list or repeating four skip-map lookups during filtering. + for (; blockIndex < blockCount - 1; blockIndex += 2) + { + CdefBlock firstBlock = blocks[blockIndex]; + CdefBlock secondBlock = blocks[blockIndex + 1]; + + Av1CdefFilter.FindDirections( + source, + firstBlock.GetSourceOffset(SourceStride, SourceBorder, unitModeInfoColumn, unitModeInfoRow, 0, 0), + secondBlock.GetSourceOffset(SourceStride, SourceBorder, unitModeInfoColumn, unitModeInfoRow, 0, 0), + SourceStride, + coefficientShift, + out directions[blockIndex], + out variances[blockIndex], + out directions[blockIndex + 1], + out variances[blockIndex + 1]); + } + + if (blockIndex < blockCount) + { + CdefBlock block = blocks[blockIndex]; + + directions[blockIndex] = Av1CdefFilter.FindDirection( + source, + block.GetSourceOffset(SourceStride, SourceBorder, unitModeInfoColumn, unitModeInfoRow, 0, 0), + SourceStride, + coefficientShift, + out variances[blockIndex]); + } + } + + if (codedStrength == 0) + { + return; + } + + for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) + { + CdefBlock block = blocks[blockIndex]; + int filteredPrimaryStrength = plane == Av1Plane.Y + ? Av1CdefFilter.AdjustStrength(primaryStrength, variances[blockIndex]) + : primaryStrength; + + if (filteredPrimaryStrength == 0 && secondaryStrength == 0) + { + continue; + } + + // Secondary-only filtering uses direction zero; otherwise chroma remaps the + // luma direction into its asymmetrically subsampled sample grid when required. + int direction = primaryStrength != 0 + ? Av1CdefFilter.ConvertDirection(directions[blockIndex], subsamplingX, subsamplingY) + : 0; + int blockPlaneColumn = (block.ModeInfoColumn << Av1Constants.ModeInfoSizeLog2) >> subsamplingX; + int blockPlaneRow = (block.ModeInfoRow << Av1Constants.ModeInfoSizeLog2) >> subsamplingY; + int blockSourceOffset = block.GetSourceOffset( + SourceStride, + SourceBorder, + unitModeInfoColumn, + unitModeInfoRow, + subsamplingX, + subsamplingY); + + int blockDestinationOffset = destinationStride + (blockPlaneRow * destinationStride) + blockPlaneColumn; + + if (this.frameBuffer.BytesPerSample == 2) + { + Av1CdefFilter.FilterBlock( + source, + blockSourceOffset, + SourceStride, + highBitDepthDestination, + blockDestinationOffset, + destinationStride, + filteredPrimaryStrength, + secondaryStrength, + direction, + damping, + damping, + coefficientShift, + blockWidth, + blockHeight); + } + else + { + Av1CdefFilter.FilterBlock( + source, + blockSourceOffset, + SourceStride, + lowBitDepthDestination, + blockDestinationOffset, + destinationStride, + filteredPrimaryStrength, + secondaryStrength, + direction, + damping, + damping, + coefficientShift, + blockWidth, + blockHeight); } } } + /// + /// Gets the byte or native 16-bit destination span for one frame plane. + /// + /// The color plane. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// Receives the byte destination for an eight-bit frame. + /// Receives the native destination for a high-bit-depth frame. + /// Receives the number of samples between adjacent rows. + private void GetPlaneDestination( + Av1Plane plane, + int subsamplingX, + int subsamplingY, + out Span lowBitDepthDestination, + out Span highBitDepthDestination, + out int destinationStride) + { + lowBitDepthDestination = default; + highBitDepthDestination = default; + + if (this.frameBuffer.BytesPerSample == 2) + { + Span signedDestination = this.frameBuffer.DeriveBlockPointer16( + plane, + Point.Empty, + subsamplingX, + subsamplingY, + out destinationStride); + + highBitDepthDestination = MemoryMarshal.Cast(signedDestination); + } + else + { + lowBitDepthDestination = this.frameBuffer.DeriveBlockPointer( + plane, + Point.Empty, + subsamplingX, + subsamplingY, + out destinationStride); + } + } + + /// + /// Copies one frame rectangle into 16-bit CDEF working storage. + /// + /// The byte source for an eight-bit frame. + /// The native source for a high-bit-depth frame. + /// The offset of the rectangle's top-left source sample. + /// The number of samples between adjacent source rows. + /// The 16-bit working destination. + /// The offset of the rectangle's top-left destination sample. + /// The number of samples between adjacent destination rows. + /// The rectangle width in samples. + /// The rectangle height in samples. + private void CopyFrameRectangle( + ReadOnlySpan lowBitDepthSource, + ReadOnlySpan highBitDepthSource, + int sourceOffset, + int sourceStride, + Span 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); + } + } + /// /// Gets the strength-table selection assigned to a 64x64 CDEF unit. /// @@ -445,23 +706,25 @@ internal class Av1CdefDecoder public int ModeInfoRow { get; } /// - /// Gets the frame-wide direction-map index for this block. - /// - /// The number of 8x8 blocks in an aligned luma row. - /// The direction-map index. - public int GetDirectionIndex(int lumaBlockColumnCount) - => ((this.ModeInfoRow >> 1) * lumaBlockColumnCount) + (this.ModeInfoColumn >> 1); - - /// - /// Gets the offset of this luma block in the bordered CDEF source plane. + /// Gets the block offset in a bordered CDEF source unit. /// /// The number of samples between adjacent source rows. /// The number of unavailable samples surrounding the source. - /// The source-plane offset. - public int GetSourceOffset(int sourceStride, int sourceBorder) + /// The unit's frame-relative column in 4x4 luma units. + /// The unit's frame-relative row in 4x4 luma units. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// The source-unit offset. + public int GetSourceOffset( + int sourceStride, + int sourceBorder, + int unitModeInfoColumn, + int unitModeInfoRow, + int subsamplingX, + int subsamplingY) { - int planeColumn = this.ModeInfoColumn << Av1Constants.ModeInfoSizeLog2; - int planeRow = this.ModeInfoRow << Av1Constants.ModeInfoSizeLog2; + int planeColumn = ((this.ModeInfoColumn - unitModeInfoColumn) << Av1Constants.ModeInfoSizeLog2) >> subsamplingX; + int planeRow = ((this.ModeInfoRow - unitModeInfoRow) << Av1Constants.ModeInfoSizeLog2) >> subsamplingY; return ((planeRow + sourceBorder) * sourceStride) + planeColumn + sourceBorder; } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainDecoder.cs index 89acc4038..6bcacdf96 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainDecoder.cs @@ -94,14 +94,14 @@ internal sealed class Av1FilmGrainDecoder int alignedWidth = Av1Math.AlignPowerOf2(visibleWidth, 1); int alignedHeight = Av1Math.AlignPowerOf2(visibleHeight, 1); - Buffer2D lumaBuffer = this.frameBuffer.BufferY!; + Buffer2D lumaBuffer = this.frameBuffer.GetPlaneBuffer(Av1Plane.Y); // 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. int lumaStride = lumaBuffer.Width / this.frameBuffer.BytesPerSample; int chromaStride = isMonochrome ? 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. // 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 cb = isMonochrome ? Span.Empty : GetPlaneSamples( - this.frameBuffer.BufferCb!, + this.frameBuffer.GetPlaneBuffer(Av1Plane.U), this.frameBuffer.OriginX >> subsamplingX, this.frameBuffer.OriginY >> subsamplingY); Span cr = isMonochrome ? Span.Empty : GetPlaneSamples( - this.frameBuffer.BufferCr!, + this.frameBuffer.GetPlaneBuffer(Av1Plane.V), this.frameBuffer.OriginX >> subsamplingX, this.frameBuffer.OriginY >> subsamplingY); @@ -160,14 +160,14 @@ internal sealed class Av1FilmGrainDecoder Span cb = isMonochrome ? Span.Empty : GetPlaneSamples( - this.frameBuffer.BufferCb!, + this.frameBuffer.GetPlaneBuffer(Av1Plane.U), this.frameBuffer.OriginX >> subsamplingX, this.frameBuffer.OriginY >> subsamplingY); Span cr = isMonochrome ? Span.Empty : GetPlaneSamples( - this.frameBuffer.BufferCr!, + this.frameBuffer.GetPlaneBuffer(Av1Plane.V), this.frameBuffer.OriginX >> subsamplingX, this.frameBuffer.OriginY >> subsamplingY); diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterContext.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterContext.cs index 046918ab6..0f5afc2b5 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterContext.cs +++ b/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.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; /// /// Stores the transform-size map consumed by the AV1 deblocking loop filter. /// -internal class Av1LoopFilterContext +internal sealed class Av1LoopFilterContext : IDisposable { /// - /// Stores luma and shared-chroma transform sizes at plane-relative 4x4 granularity. + /// Stores luma transform sizes at plane-relative 4x4 granularity. /// - private readonly Av1TransformSize[][] transformSizes = new Av1TransformSize[2][]; + private readonly MemoryGroup transformSizesY; /// - /// Stores the row stride of each transform-size map. + /// The active luma transform-map dimensions in plane-relative 4x4 units. /// - private readonly int[] transformSizeStrides = new int[2]; + private readonly Size transformSizesYSize; /// - /// Initializes a new instance of the class. + /// Stores shared-chroma transform sizes at plane-relative 4x4 granularity. /// - /// The sequence header defining aligned frame and chroma dimensions. - public Av1LoopFilterContext(ObuSequenceHeader sequenceHeader) - { - int alignedModeInfoWidth = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, sequenceHeader.SuperblockSizeLog2) >> - Av1Constants.ModeInfoSizeLog2; + private readonly MemoryGroup? transformSizesUv; - int alignedModeInfoHeight = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameHeight, sequenceHeader.SuperblockSizeLog2) >> - Av1Constants.ModeInfoSizeLog2; + /// + /// The active shared-chroma transform-map dimensions in plane-relative 4x4 units. + /// + private readonly Size transformSizesUvSize; - this.transformSizeStrides[(int)Av1PlaneType.Y] = alignedModeInfoWidth; - this.transformSizes[(int)Av1PlaneType.Y] = new Av1TransformSize[alignedModeInfoWidth * alignedModeInfoHeight]; + /// + /// Initializes a new instance of the class. + /// + /// The allocator that owns the frame-sized transform maps. + /// The sequence header defining superblock and chroma geometry. + /// The frame header defining active coded dimensions. + public Av1LoopFilterContext( + MemoryAllocator memoryAllocator, + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader) + { + int modeInfoWidth = frameHeader.ModeInfoColumnCount; + int modeInfoHeight = frameHeader.ModeInfoRowCount; + MemoryGroup? transformSizesY = null; + MemoryGroup? transformSizesUv = null; + this.transformSizesUvSize = default; - if (!sequenceHeader.ColorConfig.IsMonochrome) + try { - int subX = sequenceHeader.ColorConfig.SubSamplingX ? 1 : 0; - int subY = sequenceHeader.ColorConfig.SubSamplingY ? 1 : 0; - int chromaWidth = Av1Math.DivideLog2Ceiling(alignedModeInfoWidth, subX); - int chromaHeight = Av1Math.DivideLog2Ceiling(alignedModeInfoHeight, subY); + long lumaLength = (long)modeInfoWidth * modeInfoHeight; + transformSizesY = memoryAllocator.AllocateGroup( + lumaLength, + 1, + AllocationOptions.Clean); - this.transformSizeStrides[(int)Av1PlaneType.Uv] = chromaWidth; - this.transformSizes[(int)Av1PlaneType.Uv] = new Av1TransformSize[chromaWidth * chromaHeight]; + if (!sequenceHeader.ColorConfig.IsMonochrome) + { + 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( + 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) { int planeType = Math.Min((int)plane, (int)Av1PlaneType.Uv); - Span transformSizeMap = this.transformSizes[planeType]; - int stride = this.transformSizeStrides[planeType]; - int width = transformSize.Get4x4WideCount(); - int height = transformSize.Get4x4HighCount(); + MemoryGroup transformSizeMap; + Size transformSizeMapSize; + if (planeType == (int)Av1PlaneType.Y) + { + 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 - // edge lookup independent of the transform traversal order used while reconstructing the coded block. + transformSizeMapSize = this.transformSizesUvSize; + } + + 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++) { - 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 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) { int planeType = Math.Min((int)plane, (int)Av1PlaneType.Uv); - int stride = this.transformSizeStrides[planeType]; - return this.transformSizes[planeType][(position.Y * stride) + position.X]; + MemoryGroup transformSizeMap; + 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]; + } + + /// + /// Returns the allocator-owned transform-size maps. + /// + public void Dispose() + { + this.transformSizesUv?.Dispose(); + this.transformSizesY.Dispose(); } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterDecoder.cs index 3e3616105..8dbc55869 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterDecoder.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; /// /// Applies the AV1 in-loop deblocking stage to a reconstructed still-image frame. /// -internal class Av1LoopFilterDecoder +internal sealed class Av1LoopFilterDecoder { /// /// The sequence-level superblock and color configuration. @@ -365,7 +365,7 @@ internal class Av1LoopFilterDecoder level = Av1Math.Clip3( 0, Av1Constants.MaxLoopFilter, - level + segmentation.FeatureData[modeInfo.SegmentId, (int)feature]); + level + segmentation.GetFeatureData(modeInfo.SegmentId, (int)feature)); } if (parameters.ReferenceDeltaModeEnabled) diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationBoundary.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationBoundary.cs index ba5e9006a..37843510e 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationBoundary.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationBoundary.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration; /// /// Preserves the deblocked and frame-edge rows required by AV1 striped loop restoration. /// -internal class Av1LoopRestorationBoundary : IDisposable +internal sealed class Av1LoopRestorationBoundary : IDisposable { /// /// The height of a complete restoration processing stripe in luma samples. @@ -46,22 +46,22 @@ internal class Av1LoopRestorationBoundary : IDisposable /// /// The two preserved rows above every processing stripe, stored by plane. /// - private readonly IMemoryOwner?[] rowsAbove = new IMemoryOwner?[Av1Constants.MaxPlanes]; + private InlineArray4?> rowsAbove; /// /// The two preserved rows below every processing stripe, stored by plane. /// - private readonly IMemoryOwner?[] rowsBelow = new IMemoryOwner?[Av1Constants.MaxPlanes]; + private InlineArray4?> rowsBelow; /// /// The upscaled sample width stored for each plane boundary row. /// - private readonly int[] planeWidths = new int[Av1Constants.MaxPlanes]; + private InlineArray4 planeWidths; /// /// The number of processing stripes represented for each plane. /// - private readonly int[] stripeCounts = new int[Av1Constants.MaxPlanes]; + private InlineArray4 stripeCounts; /// /// Initializes a new instance of the class. @@ -430,13 +430,16 @@ internal class Av1LoopRestorationBoundary : IDisposable /// The first or second preserved context row. /// The selected boundary-row span. private Span GetBoundaryRow( - IMemoryOwner?[] storage, + ReadOnlySpan?> storage, int plane, int stripe, int contextRow) { int width = this.planeWidths[plane]; int offset = ((stripe * ContextRowCount) + contextRow) * width; - return storage[plane]!.Memory.Span.Slice(offset, width); + IMemoryOwner owner = storage[plane] + ?? throw new InvalidOperationException("The selected AV1 plane has no loop-restoration boundary storage."); + + return owner.Memory.Span.Slice(offset, width); } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationDecoder.cs index 9fa00f9ad..7c047c3fb 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationDecoder.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration; /// /// Applies decoded AV1 loop-restoration units to a reconstructed still-image frame. /// -internal class Av1LoopRestorationDecoder +internal sealed class Av1LoopRestorationDecoder { /// /// 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 planeLength = planeWidth * planeHeight; MemoryAllocator allocator = this.frameBuffer.MemoryAllocator; - using IMemoryOwner sourceOwner = allocator.Allocate(planeLength); - using IMemoryOwner destinationOwner = allocator.Allocate(planeLength); - Span source = sourceOwner.Memory.Span[..planeLength]; - Span destination = destinationOwner.Memory.Span[..planeLength]; + ReadOnlySpan lowBitDepthSource = default; + ReadOnlySpan highBitDepthSource = default; + int sourceStride; + if (this.frameBuffer.BytesPerSample == 2) + { + Span 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 - // destination prevents later units from observing already restored neighboring samples. - this.CopyPlaneToWorkingBuffer(plane, subsamplingX, subsamplingY, planeWidth, planeHeight, source); + highBitDepthSource = MemoryMarshal.Cast(signedPlane); + } + else + { + lowBitDepthSource = this.frameBuffer.DeriveBlockPointer( + plane, + Point.Empty, + subsamplingX, + subsamplingY, + out sourceStride); + } + + int destinationStorageLength = planeLength * this.frameBuffer.BytesPerSample; + using IMemoryOwner destinationOwner = allocator.Allocate(destinationStorageLength); + Span destinationStorage = destinationOwner.Memory.Span[..destinationStorageLength]; + Span lowBitDepthDestination = this.frameBuffer.BytesPerSample == 1 + ? destinationStorage + : default; + Span highBitDepthDestination = this.frameBuffer.BytesPerSample == 2 + ? MemoryMarshal.Cast(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. // 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 borderedLength = borderedStride * (maximumStripeHeight + (FilterBorder * 2) + WienerPadding); int wienerScratchLength = Av1WienerFilter.GetScratchLength(maximumUnitWidth, maximumStripeHeight); - using IMemoryOwner borderedSourceOwner = allocator.Allocate(borderedLength); - using IMemoryOwner wienerScratchOwner = allocator.Allocate(wienerScratchLength); - Span borderedSource = borderedSourceOwner.Memory.Span[..borderedLength]; - Span wienerScratch = wienerScratchOwner.Memory.Span[..wienerScratchLength]; + int filterOutputLength = this.frameBuffer.BytesPerSample == 1 + ? maximumUnitWidth * maximumStripeHeight + : 0; + int ushortScratchLength = borderedLength + wienerScratchLength + filterOutputLength; + using IMemoryOwner ushortScratchOwner = allocator.Allocate(ushortScratchLength); + Span ushortScratch = ushortScratchOwner.Memory.Span[..ushortScratchLength]; + Span borderedSource = ushortScratch[..borderedLength]; + Span wienerScratch = ushortScratch.Slice(borderedLength, wienerScratchLength); + Span filterOutput = ushortScratch[(borderedLength + wienerScratchLength)..]; int processingUnitWidth = Av1LoopRestorationBoundary.ProcessingStripeSize >> subsamplingX; int maximumSelfGuidedWidth = Math.Min(processingUnitWidth, maximumUnitWidth); int selfGuidedScratchLength = Av1SelfGuidedFilter.GetScratchLength(maximumSelfGuidedWidth, maximumStripeHeight); @@ -166,8 +199,11 @@ internal class Av1LoopRestorationDecoder this.FilterUnit( planeIndex, subsamplingX, - source, - destination, + lowBitDepthSource, + highBitDepthSource, + sourceStride, + lowBitDepthDestination, + highBitDepthDestination, planeWidth, planeHeight, unitX, @@ -177,6 +213,7 @@ internal class Av1LoopRestorationDecoder unit, borderedSource, wienerScratch, + filterOutput, selfGuidedScratch); unitX += unitWidth; @@ -185,7 +222,14 @@ internal class Av1LoopRestorationDecoder unitY += unadjustedUnitHeight; } - this.CopyWorkingBufferToPlane(plane, subsamplingX, subsamplingY, planeWidth, planeHeight, destination); + this.CopyWorkingBufferToPlane( + plane, + subsamplingX, + subsamplingY, + planeWidth, + planeHeight, + lowBitDepthDestination, + highBitDepthDestination); } /// @@ -193,8 +237,11 @@ internal class Av1LoopRestorationDecoder /// /// The zero-based color-plane index. /// The horizontal chroma subsampling shift. - /// The immutable post-super-resolution plane samples. - /// The restored destination plane samples. + /// The immutable byte post-super-resolution plane, when present. + /// The immutable 16-bit post-super-resolution plane, when present. + /// The number of samples between source rows. + /// The byte restoration destination, when present. + /// The native 16-bit restoration destination, when present. /// The visible plane width. /// The visible plane height. /// The unit's first plane column. @@ -204,12 +251,16 @@ internal class Av1LoopRestorationDecoder /// The decoded unit filter and coefficients. /// Reusable storage for one bordered processing stripe. /// Reusable Wiener intermediate storage. + /// Reusable native-precision filter output for an eight-bit frame. /// Reusable self-guided intermediate storage. private void FilterUnit( int plane, int subsamplingX, - ReadOnlySpan source, - Span destination, + ReadOnlySpan lowBitDepthSource, + ReadOnlySpan highBitDepthSource, + int sourceStride, + Span lowBitDepthDestination, + Span highBitDepthDestination, int planeWidth, int planeHeight, int horizontalStart, @@ -219,6 +270,7 @@ internal class Av1LoopRestorationDecoder Av1LoopRestorationUnit unit, Span borderedSource, Span wienerScratch, + Span filterOutput, Span selfGuidedScratch) { int unitWidth = horizontalEnd - horizontalStart; @@ -227,8 +279,11 @@ internal class Av1LoopRestorationDecoder // 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. CopyRectangle( - source, - destination, + lowBitDepthSource, + highBitDepthSource, + sourceStride, + lowBitDepthDestination, + highBitDepthDestination, planeWidth, horizontalStart, unitWidth, @@ -253,35 +308,53 @@ internal class Av1LoopRestorationDecoder // 64 luma samples high, with the current unit limiting only the final iteration. if (unit.FilterType == Av1RestorationFilterType.Wiener) { - int sourceStride = unitWidth + (FilterBorder * 2) + WienerPadding; - int sourceLength = sourceStride * (stripeHeight + (FilterBorder * 2) + WienerPadding); + int borderedStride = unitWidth + (FilterBorder * 2) + WienerPadding; + int sourceLength = borderedStride * (stripeHeight + (FilterBorder * 2) + WienerPadding); Span filterSource = borderedSource[..sourceLength]; this.PopulateBorderedSource( plane, frameStripe, - source, + lowBitDepthSource, + highBitDepthSource, + sourceStride, planeWidth, planeHeight, horizontalStart, unitWidth, stripeStart, stripeHeight, - sourceStride, + borderedStride, filterSource); int destinationOffset = (stripeStart * planeWidth) + horizontalStart; + Span filterDestination = highBitDepthDestination.IsEmpty + ? filterOutput[..(unitWidth * stripeHeight)] + : highBitDepthDestination[destinationOffset..]; + int filterDestinationStride = highBitDepthDestination.IsEmpty ? unitWidth : planeWidth; int scratchLength = Av1WienerFilter.GetScratchLength(unitWidth, stripeHeight); Av1WienerFilter.FilterStripe( filterSource, - sourceStride, - destination[destinationOffset..], - planeWidth, + borderedStride, + filterDestination, + filterDestinationStride, unitWidth, stripeHeight, this.frameBuffer.BitDepth.GetBitCount(), unit.WienerHorizontal, unit.WienerVertical, wienerScratch[..scratchLength]); + + if (highBitDepthDestination.IsEmpty) + { + CopyFilterOutput( + filterDestination, + filterDestinationStride, + lowBitDepthDestination, + destinationOffset, + planeWidth, + unitWidth, + stripeHeight); + } } else { @@ -293,35 +366,53 @@ internal class Av1LoopRestorationDecoder { int blockWidth = Math.Min(processingUnitWidth, unitWidth - unitColumn); int blockStart = horizontalStart + unitColumn; - int sourceStride = blockWidth + (FilterBorder * 2) + WienerPadding; - int sourceLength = sourceStride * (stripeHeight + (FilterBorder * 2) + WienerPadding); + int borderedStride = blockWidth + (FilterBorder * 2) + WienerPadding; + int sourceLength = borderedStride * (stripeHeight + (FilterBorder * 2) + WienerPadding); Span filterSource = borderedSource[..sourceLength]; this.PopulateBorderedSource( plane, frameStripe, - source, + lowBitDepthSource, + highBitDepthSource, + sourceStride, planeWidth, planeHeight, blockStart, blockWidth, stripeStart, stripeHeight, - sourceStride, + borderedStride, filterSource); int destinationOffset = (stripeStart * planeWidth) + blockStart; + Span filterDestination = highBitDepthDestination.IsEmpty + ? filterOutput[..(blockWidth * stripeHeight)] + : highBitDepthDestination[destinationOffset..]; + int filterDestinationStride = highBitDepthDestination.IsEmpty ? blockWidth : planeWidth; int scratchLength = Av1SelfGuidedFilter.GetScratchLength(blockWidth, stripeHeight); Av1SelfGuidedFilter.FilterBlock( filterSource, - sourceStride, - destination[destinationOffset..], - planeWidth, + borderedStride, + filterDestination, + filterDestinationStride, blockWidth, stripeHeight, this.frameBuffer.BitDepth.GetBitCount(), unit.SgrParameterSet, unit.SgrProjectionCoefficients, selfGuidedScratch[..scratchLength]); + + if (highBitDepthDestination.IsEmpty) + { + CopyFilterOutput( + filterDestination, + filterDestinationStride, + lowBitDepthDestination, + destinationOffset, + planeWidth, + blockWidth, + stripeHeight); + } } } @@ -334,7 +425,9 @@ internal class Av1LoopRestorationDecoder /// /// The zero-based color-plane index. /// The frame-relative processing-stripe index. - /// The immutable post-super-resolution plane samples. + /// The immutable byte post-super-resolution plane, when present. + /// The immutable 16-bit post-super-resolution plane, when present. + /// The number of samples between source rows. /// The visible plane width. /// The visible plane height. /// The first filtered plane column. @@ -346,7 +439,9 @@ internal class Av1LoopRestorationDecoder private void PopulateBorderedSource( int plane, int frameStripe, - ReadOnlySpan source, + ReadOnlySpan lowBitDepthSource, + ReadOnlySpan highBitDepthSource, + int sourceStride, int planeWidth, int planeHeight, int blockStart, @@ -361,30 +456,38 @@ internal class Av1LoopRestorationDecoder for (int destinationRow = 0; destinationRow < sourceRowCount; destinationRow++) { int sourceY = stripeStart + destinationRow - FilterBorder; - ReadOnlySpan sourceRow; + ReadOnlySpan lowBitDepthSourceRow = default; + ReadOnlySpan highBitDepthSourceRow; if (sourceY < 0) { - sourceRow = this.boundary.GetRowAbove(plane, frameStripe, 0); + highBitDepthSourceRow = this.boundary.GetRowAbove(plane, frameStripe, 0); } else if (sourceY < stripeStart) { // Two preserved deblocked rows expand to three filter rows as [0, 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) { - sourceRow = this.boundary.GetRowBelow(plane, frameStripe, 0); + highBitDepthSourceRow = this.boundary.GetRowBelow(plane, frameStripe, 0); } else if (sourceY >= stripeEnd) { // The bottom expansion is [0, 1, 1]; the padded Wiener zero tap also reads row 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 { - sourceRow = source.Slice(sourceY * planeWidth, planeWidth); + int sourceOffset = sourceStride + (sourceY * sourceStride); + lowBitDepthSourceRow = lowBitDepthSource.Slice(sourceOffset, planeWidth); + highBitDepthSourceRow = default; } Span destinationRowSpan = destination.Slice(destinationRow * destinationStride, destinationStride); @@ -395,18 +498,38 @@ internal class Av1LoopRestorationDecoder // Replication occurs only at the visible frame boundary. 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 copiedEnd = Math.Min(sourceX + destinationStride, planeWidth); int copiedLength = copiedEnd - copiedStart; - sourceRow.Slice(copiedStart, copiedLength).CopyTo(destinationRowSpan[leftExtension..]); + Span copiedDestination = destinationRowSpan.Slice(leftExtension, copiedLength); + if (!highBitDepthSourceRow.IsEmpty) + { + highBitDepthSourceRow.Slice(copiedStart, copiedLength).CopyTo(copiedDestination); + } + else + { + ReadOnlySpan copiedSource = lowBitDepthSourceRow.Slice(copiedStart, copiedLength); + for (int column = 0; column < copiedLength; column++) + { + copiedDestination[column] = copiedSource[column]; + } + } int populatedLength = leftExtension + copiedLength; 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 /// /// Copies an unfiltered restoration-unit rectangle between plane working buffers. /// - /// The immutable source plane. - /// The destination plane. + /// The immutable byte source plane, when present. + /// The immutable 16-bit source plane, when present. + /// The number of samples between source rows. + /// The byte destination, when present. + /// The native 16-bit destination, when present. /// The number of samples between plane rows. /// The first copied column. /// The number of copied columns. /// The first copied row. /// The exclusive copied row limit. private static void CopyRectangle( - ReadOnlySpan source, - Span destination, + ReadOnlySpan lowBitDepthSource, + ReadOnlySpan highBitDepthSource, + int sourceStride, + Span lowBitDepthDestination, + Span highBitDepthDestination, int planeWidth, int horizontalStart, int width, @@ -432,89 +561,69 @@ internal class Av1LoopRestorationDecoder { for (int row = verticalStart; row < verticalEnd; row++) { - int offset = (row * planeWidth) + horizontalStart; - source.Slice(offset, width).CopyTo(destination[offset..]); + int sourceOffset = sourceStride + (row * sourceStride) + horizontalStart; + 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)); + } } } /// - /// Copies one reconstructed plane into an immutable 16-bit working buffer. + /// Narrows one bounded restoration-filter output into its eight-bit frame destination. /// - /// The luma or chroma plane. - /// The horizontal chroma subsampling shift. - /// The vertical chroma subsampling shift. - /// The visible plane width. - /// The visible plane height. - /// The row-major working buffer. - private void CopyPlaneToWorkingBuffer( - Av1Plane plane, - int subsamplingX, - int subsamplingY, - int planeWidth, - int planeHeight, - Span destination) + /// The 16-bit filter output. + /// The number of samples between source rows. + /// The byte restoration destination. + /// The offset of the first destination sample. + /// The number of samples between destination rows. + /// The copied width in samples. + /// The copied height in samples. + private static void CopyFilterOutput( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationOffset, + int destinationStride, + int width, + int height) { - Span lowBitDepthPlane = default; - Span highBitDepthPlane = default; - int sourceStride; - if (this.frameBuffer.BytesPerSample == 2) + for (int row = 0; row < height; row++) { - Span signedPlane = this.frameBuffer.DeriveBlockPointer16( - plane, - Point.Empty, - subsamplingX, - subsamplingY, - out sourceStride); - - highBitDepthPlane = MemoryMarshal.Cast(signedPlane); - } - else - { - lowBitDepthPlane = this.frameBuffer.DeriveBlockPointer( - plane, - Point.Empty, - subsamplingX, - subsamplingY, - out sourceStride); - } + ReadOnlySpan sourceRow = source.Slice(row * sourceStride, width); + Span destinationRow = destination.Slice(destinationOffset + (row * destinationStride), width); - for (int row = 0; row < planeHeight; row++) - { - // 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 destinationRow = destination.Slice(row * planeWidth, planeWidth); - if (!highBitDepthPlane.IsEmpty) - { - highBitDepthPlane.Slice(frameOffset, planeWidth).CopyTo(destinationRow); - } - else + for (int column = 0; column < width; column++) { - ReadOnlySpan sourceRow = lowBitDepthPlane.Slice(frameOffset, planeWidth); - for (int column = 0; column < planeWidth; column++) - { - destinationRow[column] = sourceRow[column]; - } + // The 8-bit restoration filters clip every result to the bit-depth range before this exact narrowing. + destinationRow[column] = (byte)sourceRow[column]; } } } /// - /// Copies one restored 16-bit working buffer back to its reconstructed plane. + /// Copies one native-width restoration buffer back to its reconstructed plane. /// /// The luma or chroma plane. /// The horizontal chroma subsampling shift. /// The vertical chroma subsampling shift. /// The visible plane width. /// The visible plane height. - /// The row-major restored working buffer. + /// The row-major byte restoration buffer, when present. + /// The row-major native restoration buffer, when present. private void CopyWorkingBufferToPlane( Av1Plane plane, int subsamplingX, int subsamplingY, int planeWidth, int planeHeight, - ReadOnlySpan source) + ReadOnlySpan lowBitDepthSource, + ReadOnlySpan highBitDepthSource) { Span lowBitDepthPlane = default; Span highBitDepthPlane = default; @@ -544,18 +653,13 @@ internal class Av1LoopRestorationDecoder { // The destination view has the same preceding prediction row as the source view. int frameOffset = destinationStride + (row * destinationStride); - ReadOnlySpan sourceRow = source.Slice(row * planeWidth, planeWidth); if (!highBitDepthPlane.IsEmpty) { - sourceRow.CopyTo(highBitDepthPlane[frameOffset..]); + highBitDepthSource.Slice(row * planeWidth, planeWidth).CopyTo(highBitDepthPlane[frameOffset..]); } else { - Span destinationRow = lowBitDepthPlane.Slice(frameOffset, planeWidth); - for (int column = 0; column < planeWidth; column++) - { - destinationRow[column] = (byte)sourceRow[column]; - } + lowBitDepthSource.Slice(row * planeWidth, planeWidth).CopyTo(lowBitDepthPlane[frameOffset..]); } } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1DeQuantizationContext.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1DeQuantizationContext.cs index 13484d8bb..5ecf17ad7 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1DeQuantizationContext.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1DeQuantizationContext.cs @@ -8,17 +8,17 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; /// /// Stores the AV1 DC and AC dequantization values for every segment and color plane in a frame. /// -internal class Av1DeQuantizationContext +internal sealed class Av1DeQuantizationContext { /// /// The DC dequantization values indexed by segment and then plane. /// - private readonly short[][] dcContent; + private InlineArray8> dcContent; /// /// The AC dequantization values indexed by segment and then plane. /// - private readonly short[][] acContent; + private InlineArray8> acContent; /// /// Initializes a new instance of the class from the frame's base quantizer, @@ -29,12 +29,8 @@ internal class Av1DeQuantizationContext public Av1DeQuantizationContext(ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) { 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++) { - this.dcContent[segmentId] = new short[Av1Constants.MaxPlanes]; - this.acContent[segmentId] = new short[Av1Constants.MaxPlanes]; int qindex = Av1QuantizationLookup.GetQIndex(frameHeader.SegmentationParameters, segmentId, frameHeader.QuantizationParameters.BaseQIndex); for (int plane = 0; plane < Av1Constants.MaxPlanes; plane++) diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizationLookup.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizationLookup.cs index f91692f24..b5390bc9d 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizationLookup.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizationLookup.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; /// /// Provides the normative AV1 inverse quantization matrices for each matrix level, plane class, and transform size. /// -internal class Av1InverseQuantizationLookup +internal static class Av1InverseQuantizationLookup { /// /// Maps each AV1 transform size to its stored matrix index; sizes with a 64-pixel dimension reuse the adjusted 32-pixel matrix. diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizer.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizer.cs index 8c5aad539..cb2eb6382 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizer.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizer.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; /// /// Reconstructs AV1 transform coefficients from quantized coefficient levels. /// -internal class Av1InverseQuantizer +internal sealed class Av1InverseQuantizer { /// /// The sequence-level color configuration that determines coefficient precision. @@ -47,7 +47,6 @@ internal class Av1InverseQuantizer public void UpdateDequant(Av1DeQuantizationContext deQuants, Av1SuperblockInfo superblockInfo) { Av1BitDepth bitDepth = this.sequenceHeader.ColorConfig.BitDepth; - Guard.NotNull(deQuants, nameof(deQuants)); this.deQuantsDeltaQ = deQuants; if (this.frameHeader.DeltaQParameters.IsPresent) { @@ -82,7 +81,6 @@ internal class Av1InverseQuantizer /// The number of coefficient levels consumed. public int InverseQuantize(Av1BlockModeInfo mode, Span level, Span qCoefficients, Av1TransformType transformType, Av1TransformSize transformSize, Av1Plane plane) { - Guard.NotNull(this.deQuantsDeltaQ); Av1ScanOrder scanOrder = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType); ReadOnlySpan scanIndices = scanOrder.Scan; diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1QuantizationLookup.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1QuantizationLookup.cs index 7d5170324..0e2d30f29 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1QuantizationLookup.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1QuantizationLookup.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; /// /// Provides the normative AV1 DC and AC dequantization values for each quantizer index and supported bit depth. /// -internal class Av1QuantizationLookup +internal static class Av1QuantizationLookup { // Coefficient scaling and quantization with AV1 TX are tailored to // 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)) { - int data = segmentationParameters.FeatureData[segmentId, (int)ObuSegmentationLevelFeature.AlternativeQuantizer]; + int data = segmentationParameters.GetFeatureData(segmentId, (int)ObuSegmentationLevelFeature.AlternativeQuantizer); int qIndex = baseQIndex + data; return Av1Math.Clamp(qIndex, 0, Av1Constants.MaxQ); } @@ -242,9 +242,6 @@ internal class Av1QuantizationLookup ObuQuantizationParameters quantization = frameHeader.QuantizationParameters; ObuSegmentationParameters segmentation = frameHeader.SegmentationParameters; 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++) { int qIndex = GetQIndex(segmentation, segmentId, quantization.BaseQIndex); diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/SuperResolution/Av1SuperResolutionDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/SuperResolution/Av1SuperResolutionDecoder.cs index 73a6f2d19..aa80a8011 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/SuperResolution/Av1SuperResolutionDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/SuperResolution/Av1SuperResolutionDecoder.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.SuperResolution; /// /// Applies normative AV1 horizontal super-resolution upscaling to a reconstructed still-image frame. /// -internal class Av1SuperResolutionDecoder +internal sealed class Av1SuperResolutionDecoder { /// /// The sequence-level bit-depth and color-plane configuration. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1BottomRightTopLeftConstants.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1BottomRightTopLeftConstants.cs index 0591e8522..919452ce1 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1BottomRightTopLeftConstants.cs +++ b/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. /// Separate tables preserve the alternate visit order used by mixed vertical partitions. /// -internal class Av1BottomRightTopLeftConstants +internal static class Av1BottomRightTopLeftConstants { // 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 @@ -302,43 +302,6 @@ internal class Av1BottomRightTopLeftConstants // There are tables for each of the square sizes. Vertical rectangles (like // BLOCK_16X32) use their respective "non-vert" table - /// - /// Maps supported block-size values to top-right availability tables for mixed vertical partition traversal. - /// - /// - /// Null entries identify block sizes that cannot use this traversal table; vertical rectangles reuse their standard tables. - /// - 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 // pixels are available. @@ -620,42 +583,6 @@ internal class Av1BottomRightTopLeftConstants // There are tables for each of the square sizes. Vertical rectangles (like // BLOCK_16X32) use their respective "non-vert" table - /// - /// Maps supported block-size values to bottom-left availability tables for mixed vertical partition traversal. - /// - /// - /// Null entries identify block sizes that cannot use this traversal table; vertical rectangles reuse their standard tables. - /// - 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]; - /// /// Determines whether the top-right reference samples are available for a block at the specified traversal index. /// @@ -669,7 +596,7 @@ internal class Av1BottomRightTopLeftConstants // remainder selects the bit within that byte. int index1 = blockIndex / 8; int index2 = blockIndex % 8; - Span hasTopRightTable = GetHasTopRightTable(partitionType, blockSize); + ReadOnlySpan hasTopRightTable = GetHasTopRightTable(partitionType, blockSize); return ((hasTopRightTable[index1] >> index2) & 1) > 0; } @@ -686,7 +613,7 @@ internal class Av1BottomRightTopLeftConstants // remainder selects the bit within that byte. int index1 = blockIndex / 8; int index2 = blockIndex % 8; - Span hasBottomLeftTable = GetHasBottomLeftTable(partitionType, blockSize); + ReadOnlySpan hasBottomLeftTable = GetHasBottomLeftTable(partitionType, blockSize); return ((hasBottomLeftTable[index1] >> index2) & 1) > 0; } @@ -696,23 +623,30 @@ internal class Av1BottomRightTopLeftConstants /// The partition type that determines the block traversal order. /// The block size whose availability table is selected. /// The packed top-right availability table. - private static Span GetHasTopRightTable(Av1PartitionType partition, Av1BlockSize blockSize) + private static ReadOnlySpan GetHasTopRightTable(Av1PartitionType partition, Av1BlockSize blockSize) { - byte[]? ret; - // If this is a mixed vertical partition, look up block size in vertical order. if (partition is Av1PartitionType.VerticalA or Av1PartitionType.VerticalB) { - DebugGuard.MustBeLessThan((int)blockSize, (int)Av1BlockSize.SizeS, nameof(blockSize)); - ret = HasTopRightVerticalTables[(int)blockSize]; - } - else - { - ret = HasTopRightTables[(int)blockSize]; + // libaom asserts that mixed-vertical traversal can select only vertical rectangles or squares. + // Listing those shapes directly keeps the impossible horizontal-rectangle states out of the table type. + return blockSize switch + { + Av1BlockSize.Block4x8 => HasTopRight4x8, + 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 ret; + return HasTopRightTables[(int)blockSize]; } /// @@ -721,22 +655,28 @@ internal class Av1BottomRightTopLeftConstants /// The partition type that determines the block traversal order. /// The block size whose availability table is selected. /// The packed bottom-left availability table. - private static Span GetHasBottomLeftTable(Av1PartitionType partition, Av1BlockSize blockSize) + private static ReadOnlySpan GetHasBottomLeftTable(Av1PartitionType partition, Av1BlockSize blockSize) { - byte[]? ret; - // If this is a mixed vertical partition, look up block size in vertical order. if (partition is Av1PartitionType.VerticalA or Av1PartitionType.VerticalB) { - DebugGuard.MustBeLessThan((int)blockSize, (int)Av1BlockSize.SizeS, nameof(blockSize)); - ret = HasBottomLeftVerticalTables[(int)blockSize]; - } - else - { - ret = HasBottomLeftTables[(int)blockSize]; + // The valid block shapes mirror the top-right table and the libaom traversal assertion. + return blockSize switch + { + Av1BlockSize.Block4x8 => HasBottomLeft4x8, + Av1BlockSize.Block8x8 => HasBottomLeftVertical8x8, + 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 ret; + return HasBottomLeftTables[(int)blockSize]; } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalIntraPredictor.Operations.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalIntraPredictor.Operations.cs index 927fb98eb..9cc792a1a 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalIntraPredictor.Operations.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalIntraPredictor.Operations.cs @@ -575,7 +575,7 @@ internal static partial class Av1DirectionalIntraPredictor else { 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 { 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 { 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 { 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); } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.HorizontalOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.HorizontalOperator.cs similarity index 97% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.HorizontalOperator.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.HorizontalOperator.cs index a831d80e2..1cb367266 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.HorizontalOperator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.HorizontalOperator.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; /// /// Provides horizontal intra prediction for scalar and SIMD sample representations. /// -internal abstract partial class Av1IntraPredictorBase +internal abstract partial class Av1NonDirectionalIntraPredictorBase { /// /// Implements AV1 horizontal intra prediction for scalar and SIMD lanes. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.Operator.cs similarity index 99% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.Operator.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.Operator.cs index 2ddfa692e..59c509707 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.Operator.cs +++ b/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 /// their shared width-progressive SIMD traversal. /// -internal abstract partial class Av1IntraPredictorBase +internal abstract partial class Av1NonDirectionalIntraPredictorBase { /// /// 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. declares which references it uses; /// because the operator type is closed, the JIT can remove unused loads and broadcasts from each prediction mode. /// - internal sealed class Av1IntraPredictor : Av1IntraPredictorBase + internal sealed class Av1NonDirectionalIntraPredictor : Av1NonDirectionalIntraPredictorBase where TOperator : struct, IAv1IntraPredictionOperator { /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.Packing.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.Packing.cs similarity index 99% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.Packing.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.Packing.cs index 4414925e2..f425e5609 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.Packing.cs +++ b/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 /// unsigned narrowing and the high-bit-depth overloads may use signed narrowing without additional saturation logic. /// -internal abstract partial class Av1IntraPredictorBase +internal abstract partial class Av1NonDirectionalIntraPredictorBase { /// /// Widens sixteen 8-bit samples into four 32-bit vectors. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.PaethOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.PaethOperator.cs similarity index 99% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.PaethOperator.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.PaethOperator.cs index 733d3b160..b6f9b13cd 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.PaethOperator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.PaethOperator.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; /// /// Provides Paeth intra prediction for scalar and SIMD sample representations. /// -internal abstract partial class Av1IntraPredictorBase +internal abstract partial class Av1NonDirectionalIntraPredictorBase { /// /// Implements AV1 Paeth intra prediction for scalar and SIMD lanes. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.SmoothHorizontalOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothHorizontalOperator.cs similarity index 99% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.SmoothHorizontalOperator.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothHorizontalOperator.cs index 05f9cba74..01122bf6a 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.SmoothHorizontalOperator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothHorizontalOperator.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; /// /// Provides horizontal smooth intra prediction for scalar and SIMD sample representations. /// -internal abstract partial class Av1IntraPredictorBase +internal abstract partial class Av1NonDirectionalIntraPredictorBase { /// /// Implements horizontal AV1 smooth intra prediction for scalar and SIMD lanes. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.SmoothOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothOperator.cs similarity index 99% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.SmoothOperator.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothOperator.cs index c7774a28d..00393f480 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.SmoothOperator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothOperator.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; /// /// Provides two-dimensional smooth intra prediction for scalar and SIMD sample representations. /// -internal abstract partial class Av1IntraPredictorBase +internal abstract partial class Av1NonDirectionalIntraPredictorBase { /// /// Implements two-dimensional AV1 smooth intra prediction for scalar and SIMD lanes. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.SmoothVerticalOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothVerticalOperator.cs similarity index 99% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.SmoothVerticalOperator.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothVerticalOperator.cs index e7b5c7acd..60099f0dc 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.SmoothVerticalOperator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothVerticalOperator.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; /// /// Provides vertical smooth intra prediction for scalar and SIMD sample representations. /// -internal abstract partial class Av1IntraPredictorBase +internal abstract partial class Av1NonDirectionalIntraPredictorBase { /// /// Implements vertical AV1 smooth intra prediction for scalar and SIMD lanes. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.VerticalOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.VerticalOperator.cs similarity index 97% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.VerticalOperator.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.VerticalOperator.cs index d02857261..7c90f2f43 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictor.VerticalOperator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.VerticalOperator.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; /// /// Provides vertical intra prediction for scalar and SIMD sample representations. /// -internal abstract partial class Av1IntraPredictorBase +internal abstract partial class Av1NonDirectionalIntraPredictorBase { /// /// Implements AV1 vertical intra prediction for scalar and SIMD lanes. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictorBase.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictorBase.cs similarity index 87% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictorBase.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictorBase.cs index e79b5b3a8..86f8b9186 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1IntraPredictorBase.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictorBase.cs @@ -11,37 +11,37 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; /// /// The implementation covers the non-directional prediction processes in section 7.11.2 of the AV1 specification. /// -internal abstract partial class Av1IntraPredictorBase +internal abstract partial class Av1NonDirectionalIntraPredictorBase { /// /// The horizontal prediction operator. /// - private static readonly Av1IntraPredictor HorizontalPredictor = new(); + private static readonly Av1NonDirectionalIntraPredictor HorizontalPredictor = new(); /// /// The vertical prediction operator. /// - private static readonly Av1IntraPredictor VerticalPredictor = new(); + private static readonly Av1NonDirectionalIntraPredictor VerticalPredictor = new(); /// /// The Paeth prediction operator. /// - private static readonly Av1IntraPredictor PaethPredictor = new(); + private static readonly Av1NonDirectionalIntraPredictor PaethPredictor = new(); /// /// The two-dimensional smooth prediction operator. /// - private static readonly Av1IntraPredictor SmoothPredictor = new(); + private static readonly Av1NonDirectionalIntraPredictor SmoothPredictor = new(); /// /// The horizontal smooth prediction operator. /// - private static readonly Av1IntraPredictor SmoothHorizontalPredictor = new(); + private static readonly Av1NonDirectionalIntraPredictor SmoothHorizontalPredictor = new(); /// /// The vertical smooth prediction operator. /// - private static readonly Av1IntraPredictor SmoothVerticalPredictor = new(); + private static readonly Av1NonDirectionalIntraPredictor SmoothVerticalPredictor = new(); /// /// Gets the Q8 smooth weights for every supported block dimension. @@ -73,7 +73,7 @@ internal abstract partial class Av1IntraPredictorBase /// /// The decoded non-directional prediction mode. /// The predictor for . - public static Av1IntraPredictorBase GetPredictor(Av1PredictionMode mode) + public static Av1NonDirectionalIntraPredictorBase GetPredictor(Av1PredictionMode mode) => mode switch { Av1PredictionMode.Horizontal => HorizontalPredictor, diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs index 4a11169e9..5ed8ed227 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs +++ b/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 /// continuations handle only incomplete vectors. The completed edges then feed the closed prediction operators. /// -internal class Av1PredictionDecoder +internal sealed class Av1PredictionDecoder { /// /// The largest edge length for which AV1 permits intra-edge upsampling. @@ -60,17 +60,28 @@ internal class Av1PredictionDecoder /// private readonly Memory predictorScratch; + /// + /// The complete decoder-session palette color-index map state, when supplied by a decoder session. + /// + private readonly Av1TileReader.PaletteColorIndexMaps? paletteColorIndexMaps; + /// /// Initializes a new instance of the class. /// /// The decoded sequence header for the current image. /// The decoded frame header for the current image. /// The reusable predictor workspace owned by the containing block decoder. - public Av1PredictionDecoder(ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, Memory predictorScratch) + /// The complete decoder-session palette map state. + public Av1PredictionDecoder( + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Memory predictorScratch, + Av1TileReader.PaletteColorIndexMaps? paletteColorIndexMaps = null) { this.sequenceHeader = sequenceHeader; this.frameHeader = frameHeader; this.predictorScratch = predictorScratch; + this.paletteColorIndexMaps = paletteColorIndexMaps; } /// @@ -462,7 +473,18 @@ internal class Av1PredictionDecoder if (usePalette) { ReadOnlySpan paletteColors = modeInfo.GetPaletteColors(plane); - Buffer2DRegion 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 colorIndexBuffer = plane == Av1Plane.Y + ? paletteColorIndexMaps.Luma + : paletteColorIndexMaps.Chroma; + + Buffer2DRegion colorIndexMap = modeInfo.GetPaletteColorIndexMap(plane, colorIndexBuffer); Buffer2DRegion transformColorIndexMap = colorIndexMap.GetSubRegion( blockModeInfoColumnOffset << Av1Constants.ModeInfoSizeLog2, blockModeInfoRowOffset << Av1Constants.ModeInfoSizeLog2, @@ -1215,7 +1237,7 @@ internal class Av1PredictionDecoder { int width = transformSize.GetWidth(); int height = transformSize.GetHeight(); - Av1IntraPredictorBase predictor = Av1IntraPredictorBase.GetPredictor(mode); + Av1NonDirectionalIntraPredictorBase predictor = Av1NonDirectionalIntraPredictorBase.GetPredictor(mode); if (typeof(T) == typeof(byte)) { @@ -2004,7 +2026,7 @@ internal class Av1PredictionDecoder } 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; return uvMode is Av1ChromaPredictionMode.Smooth or Av1ChromaPredictionMode.SmoothVertical or diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.Operations.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.Operations.cs index d2376fbcf..a5f25b665 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.Operations.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.Operations.cs @@ -29,7 +29,7 @@ internal partial class Av1ChromaFromLumaContext private void StoreSamples(ReadOnlySpan input, int inputStride, int outputOffset, int width, int height) { 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) { @@ -181,7 +181,7 @@ internal partial class Av1ChromaFromLumaContext private void StoreSamples(ReadOnlySpan input, int inputStride, int outputOffset, int width, int height) { 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) { @@ -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 // the sample count is accumulated first to implement the normative nearest-integer rounding. 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.Count) { diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.cs index 933578e32..d3641d600 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.cs @@ -11,13 +11,23 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; /// /// Accumulates subsampled luma samples and derives the zero-mean Q3 predictor surface used by AV1 chroma-from-luma prediction. /// -internal partial class Av1ChromaFromLumaContext +internal sealed partial class Av1ChromaFromLumaContext { /// /// The fixed row stride and maximum dimension, in chroma samples, of the luma predictor buffer. /// private const int BufferLine = 32; + /// + /// The number of samples in the fixed-stride chroma-from-luma workspace. + /// + public const int BufferLength = BufferLine * BufferLine; + + /// + /// The caller-owned fixed-stride luma predictor workspace. + /// + private readonly Memory q3Buffer; + /// /// The number of initialized predictor rows currently stored in . /// @@ -43,16 +53,26 @@ internal partial class Av1ChromaFromLumaContext /// /// The AV1 color configuration that supplies chroma subsampling. public Av1ChromaFromLumaContext(ObuColorConfig colorConfig) + : this(colorConfig, new short[BufferLength]) + { + } + + /// + /// Initializes a new instance of the class over caller-owned workspace. + /// + /// The AV1 color configuration that supplies chroma subsampling. + /// The fixed-stride signed Q3 workspace retained for the context lifetime. + public Av1ChromaFromLumaContext(ObuColorConfig colorConfig, Memory q3Buffer) { this.subX = colorConfig.SubSamplingX; this.subY = colorConfig.SubSamplingY; - this.Q3Buffer = new short[BufferLine * BufferLine]; + this.q3Buffer = q3Buffer; } /// /// Gets the fixed-stride luma predictor samples in signed Q3 fixed-point representation. /// - public short[] Q3Buffer { get; } + public Span Q3Buffer => this.q3Buffer.Span; /// /// 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 differenceHeight = height - this.bufferHeight; + Span q3Buffer = this.Q3Buffer; if (differenceWidth > 0) { @@ -163,8 +184,8 @@ internal partial class Av1ChromaFromLumaContext for (int y = 0; y < minimumHeight; y++) { int rowOffset = y * BufferLine; - short lastPixel = this.Q3Buffer[rowOffset + this.bufferWidth - 1]; - this.Q3Buffer.AsSpan(rowOffset + this.bufferWidth, differenceWidth).Fill(lastPixel); + short lastPixel = q3Buffer[rowOffset + this.bufferWidth - 1]; + q3Buffer.Slice(rowOffset + this.bufferWidth, differenceWidth).Fill(lastPixel); } this.bufferWidth = width; @@ -176,7 +197,7 @@ internal partial class Av1ChromaFromLumaContext for (int y = this.bufferHeight; y < height; y++) { 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundAveragePredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundAveragePredictor.Operator.cs index c14a44155..7b2b721c0 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundAveragePredictor.Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundAveragePredictor.Operator.cs @@ -4,7 +4,7 @@ using System.Runtime.CompilerServices; 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundAveragePredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundAveragePredictor.cs index 1b3e17536..9d7390b0b 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundAveragePredictor.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundAveragePredictor.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeightedPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeightedPredictor.Operator.cs index 5cd6247cf..43ae86623 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeightedPredictor.Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeightedPredictor.Operator.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; 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; @@ -119,9 +119,9 @@ internal static partial class Av1CompoundDistanceWeightedPredictor [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector128 Blend(Vector128 first, Vector128 second, int firstWeight, int secondWeight) { - Av1IntraPredictorBase.Widen(first, out Vector128 first0, out Vector128 first1, out Vector128 first2, out Vector128 first3); - Av1IntraPredictorBase.Widen(second, out Vector128 second0, out Vector128 second1, out Vector128 second2, out Vector128 second3); - return Av1IntraPredictorBase.Narrow( + Av1NonDirectionalIntraPredictorBase.Widen(first, out Vector128 first0, out Vector128 first1, out Vector128 first2, out Vector128 first3); + Av1NonDirectionalIntraPredictorBase.Widen(second, out Vector128 second0, out Vector128 second1, out Vector128 second2, out Vector128 second3); + return Av1NonDirectionalIntraPredictorBase.Narrow( Blend(first0, second0, firstWeight, secondWeight), Blend(first1, second1, firstWeight, secondWeight), Blend(first2, second2, firstWeight, secondWeight), @@ -132,9 +132,9 @@ internal static partial class Av1CompoundDistanceWeightedPredictor [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 Blend(Vector256 first, Vector256 second, int firstWeight, int secondWeight) { - Av1IntraPredictorBase.Widen(first, out Vector256 first0, out Vector256 first1, out Vector256 first2, out Vector256 first3); - Av1IntraPredictorBase.Widen(second, out Vector256 second0, out Vector256 second1, out Vector256 second2, out Vector256 second3); - return Av1IntraPredictorBase.Narrow( + Av1NonDirectionalIntraPredictorBase.Widen(first, out Vector256 first0, out Vector256 first1, out Vector256 first2, out Vector256 first3); + Av1NonDirectionalIntraPredictorBase.Widen(second, out Vector256 second0, out Vector256 second1, out Vector256 second2, out Vector256 second3); + return Av1NonDirectionalIntraPredictorBase.Narrow( Blend(first0, second0, firstWeight, secondWeight), Blend(first1, second1, firstWeight, secondWeight), Blend(first2, second2, firstWeight, secondWeight), @@ -145,9 +145,9 @@ internal static partial class Av1CompoundDistanceWeightedPredictor [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector512 Blend(Vector512 first, Vector512 second, int firstWeight, int secondWeight) { - Av1IntraPredictorBase.Widen(first, out Vector512 first0, out Vector512 first1, out Vector512 first2, out Vector512 first3); - Av1IntraPredictorBase.Widen(second, out Vector512 second0, out Vector512 second1, out Vector512 second2, out Vector512 second3); - return Av1IntraPredictorBase.Narrow( + Av1NonDirectionalIntraPredictorBase.Widen(first, out Vector512 first0, out Vector512 first1, out Vector512 first2, out Vector512 first3); + Av1NonDirectionalIntraPredictorBase.Widen(second, out Vector512 second0, out Vector512 second1, out Vector512 second2, out Vector512 second3); + return Av1NonDirectionalIntraPredictorBase.Narrow( Blend(first0, second0, firstWeight, secondWeight), Blend(first1, second1, firstWeight, secondWeight), Blend(first2, second2, firstWeight, secondWeight), @@ -158,9 +158,9 @@ internal static partial class Av1CompoundDistanceWeightedPredictor [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector128 Blend(Vector128 first, Vector128 second, int firstWeight, int secondWeight) { - Av1IntraPredictorBase.Widen(first.AsInt16(), out Vector128 first0, out Vector128 first1); - Av1IntraPredictorBase.Widen(second.AsInt16(), out Vector128 second0, out Vector128 second1); - return Av1IntraPredictorBase.Narrow( + Av1NonDirectionalIntraPredictorBase.Widen(first.AsInt16(), out Vector128 first0, out Vector128 first1); + Av1NonDirectionalIntraPredictorBase.Widen(second.AsInt16(), out Vector128 second0, out Vector128 second1); + return Av1NonDirectionalIntraPredictorBase.Narrow( Blend(first0, second0, firstWeight, secondWeight), Blend(first1, second1, firstWeight, secondWeight)).AsUInt16(); } @@ -169,9 +169,9 @@ internal static partial class Av1CompoundDistanceWeightedPredictor [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 Blend(Vector256 first, Vector256 second, int firstWeight, int secondWeight) { - Av1IntraPredictorBase.Widen(first.AsInt16(), out Vector256 first0, out Vector256 first1); - Av1IntraPredictorBase.Widen(second.AsInt16(), out Vector256 second0, out Vector256 second1); - return Av1IntraPredictorBase.Narrow( + Av1NonDirectionalIntraPredictorBase.Widen(first.AsInt16(), out Vector256 first0, out Vector256 first1); + Av1NonDirectionalIntraPredictorBase.Widen(second.AsInt16(), out Vector256 second0, out Vector256 second1); + return Av1NonDirectionalIntraPredictorBase.Narrow( Blend(first0, second0, firstWeight, secondWeight), Blend(first1, second1, firstWeight, secondWeight)).AsUInt16(); } @@ -180,9 +180,9 @@ internal static partial class Av1CompoundDistanceWeightedPredictor [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector512 Blend(Vector512 first, Vector512 second, int firstWeight, int secondWeight) { - Av1IntraPredictorBase.Widen(first.AsInt16(), out Vector512 first0, out Vector512 first1); - Av1IntraPredictorBase.Widen(second.AsInt16(), out Vector512 second0, out Vector512 second1); - return Av1IntraPredictorBase.Narrow( + Av1NonDirectionalIntraPredictorBase.Widen(first.AsInt16(), out Vector512 first0, out Vector512 first1); + Av1NonDirectionalIntraPredictorBase.Widen(second.AsInt16(), out Vector512 second0, out Vector512 second1); + return Av1NonDirectionalIntraPredictorBase.Narrow( Blend(first0, second0, firstWeight, secondWeight), Blend(first1, second1, firstWeight, secondWeight)).AsUInt16(); } diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeightedPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeightedPredictor.cs index c315d161e..7ea35e7e9 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeightedPredictor.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeightedPredictor.cs @@ -6,7 +6,7 @@ using System.Runtime.InteropServices; using System.Runtime.Intrinsics; 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Operator.cs index 13bd264c8..9bef8f205 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Operator.cs @@ -4,7 +4,7 @@ using System.Runtime.CompilerServices; 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; @@ -430,7 +430,7 @@ internal static partial class Av1CompoundInterPredictor int preShift, int round, int roundOffset) - => Av1IntraPredictorBase.Narrow( + => Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(lower << preShift, round) + Vector128.Create(roundOffset), RoundPowerOfTwo(upper << preShift, round) + Vector128.Create(roundOffset)).AsUInt16(); @@ -442,7 +442,7 @@ internal static partial class Av1CompoundInterPredictor int preShift, int round, int roundOffset) - => Av1IntraPredictorBase.Narrow( + => Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(lower << preShift, round) + Vector256.Create(roundOffset), RoundPowerOfTwo(upper << preShift, round) + Vector256.Create(roundOffset)).AsUInt16(); @@ -454,7 +454,7 @@ internal static partial class Av1CompoundInterPredictor int preShift, int round, int roundOffset) - => Av1IntraPredictorBase.Narrow( + => Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(lower << preShift, round) + Vector512.Create(roundOffset), RoundPowerOfTwo(upper << preShift, round) + Vector512.Create(roundOffset)).AsUInt16(); @@ -466,21 +466,21 @@ internal static partial class Av1CompoundInterPredictor /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector128 PrepareHorizontal(Vector128 lower, Vector128 upper) - => Av1IntraPredictorBase.Narrow( + => Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(lower + Vector128.Create(HorizontalBias), Round0Bits), RoundPowerOfTwo(upper + Vector128.Create(HorizontalBias), Round0Bits)); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 PrepareHorizontal(Vector256 lower, Vector256 upper) - => Av1IntraPredictorBase.Narrow( + => Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(lower + Vector256.Create(HorizontalBias), Round0Bits), RoundPowerOfTwo(upper + Vector256.Create(HorizontalBias), Round0Bits)); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector512 PrepareHorizontal(Vector512 lower, Vector512 upper) - => Av1IntraPredictorBase.Narrow( + => Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(lower + Vector512.Create(HorizontalBias), Round0Bits), RoundPowerOfTwo(upper + Vector512.Create(HorizontalBias), Round0Bits)); @@ -496,7 +496,7 @@ internal static partial class Av1CompoundInterPredictor Vector128 upper, int bias, int round) - => Av1IntraPredictorBase.Narrow( + => Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(lower + Vector128.Create(bias), round), RoundPowerOfTwo(upper + Vector128.Create(bias), round)); @@ -507,7 +507,7 @@ internal static partial class Av1CompoundInterPredictor Vector256 upper, int bias, int round) - => Av1IntraPredictorBase.Narrow( + => Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(lower + Vector256.Create(bias), round), RoundPowerOfTwo(upper + Vector256.Create(bias), round)); @@ -518,7 +518,7 @@ internal static partial class Av1CompoundInterPredictor Vector512 upper, int bias, int round) - => Av1IntraPredictorBase.Narrow( + => Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(lower + Vector512.Create(bias), round), RoundPowerOfTwo(upper + Vector512.Create(bias), round)); @@ -530,21 +530,21 @@ internal static partial class Av1CompoundInterPredictor /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector128 PrepareVertical(Vector128 lower, Vector128 upper) - => Av1IntraPredictorBase.Narrow( + => Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(lower + Vector128.Create(VerticalBias), CompoundRound1Bits), RoundPowerOfTwo(upper + Vector128.Create(VerticalBias), CompoundRound1Bits)).AsUInt16(); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 PrepareVertical(Vector256 lower, Vector256 upper) - => Av1IntraPredictorBase.Narrow( + => Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(lower + Vector256.Create(VerticalBias), CompoundRound1Bits), RoundPowerOfTwo(upper + Vector256.Create(VerticalBias), CompoundRound1Bits)).AsUInt16(); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector512 PrepareVertical(Vector512 lower, Vector512 upper) - => Av1IntraPredictorBase.Narrow( + => Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(lower + Vector512.Create(VerticalBias), CompoundRound1Bits), RoundPowerOfTwo(upper + Vector512.Create(VerticalBias), CompoundRound1Bits)).AsUInt16(); @@ -559,7 +559,7 @@ internal static partial class Av1CompoundInterPredictor Vector128 lower, Vector128 upper, int bias) - => Av1IntraPredictorBase.Narrow( + => Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(lower + Vector128.Create(bias), CompoundRound1Bits), RoundPowerOfTwo(upper + Vector128.Create(bias), CompoundRound1Bits)).AsUInt16(); @@ -569,7 +569,7 @@ internal static partial class Av1CompoundInterPredictor Vector256 lower, Vector256 upper, int bias) - => Av1IntraPredictorBase.Narrow( + => Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(lower + Vector256.Create(bias), CompoundRound1Bits), RoundPowerOfTwo(upper + Vector256.Create(bias), CompoundRound1Bits)).AsUInt16(); @@ -579,7 +579,7 @@ internal static partial class Av1CompoundInterPredictor Vector512 lower, Vector512 upper, int bias) - => Av1IntraPredictorBase.Narrow( + => Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(lower + Vector512.Create(bias), CompoundRound1Bits), RoundPowerOfTwo(upper + Vector512.Create(bias), CompoundRound1Bits)).AsUInt16(); } diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.cs index 53ea21d18..2ba6fcf20 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.Operator.cs index 310e389fc..6a6eb78c2 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.Operator.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.cs index 8a099ec1b..b1e0f95b7 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.cs @@ -6,7 +6,7 @@ using System.Runtime.InteropServices; using System.Runtime.Intrinsics; 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDifferenceWeightedMaskBuilder.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDifferenceWeightedMaskBuilder.Operator.cs index 17a7a4c7f..9b0adeb5a 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDifferenceWeightedMaskBuilder.Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDifferenceWeightedMaskBuilder.Operator.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDifferenceWeightedMaskBuilder.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDifferenceWeightedMaskBuilder.cs index 128689f85..eaf6d72f1 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDifferenceWeightedMaskBuilder.cs +++ b/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 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.Operator.cs index aa46f9beb..f876c16de 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.Operator.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.cs index 9598d95f5..15779f3dc 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.cs @@ -6,7 +6,7 @@ using System.Runtime.InteropServices; using System.Runtime.Intrinsics; 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateMaskBlendPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateMaskBlendPredictor.Operator.cs index 6ffd9e7f2..10feeaad4 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateMaskBlendPredictor.Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateMaskBlendPredictor.Operator.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateMaskBlendPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateMaskBlendPredictor.cs index 04e82a059..967e11a8a 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateMaskBlendPredictor.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateMaskBlendPredictor.cs @@ -6,7 +6,7 @@ using System.Runtime.InteropServices; using System.Runtime.Intrinsics; 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundMaskBlendPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundMaskBlendPredictor.Operator.cs index 43889c979..d7df3a32a 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundMaskBlendPredictor.Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundMaskBlendPredictor.Operator.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; 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; @@ -111,10 +111,10 @@ internal static partial class Av1CompoundMaskBlendPredictor [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector128 Blend(Vector128 first, Vector128 second, Vector128 alpha) { - Av1IntraPredictorBase.Widen(first, out Vector128 first0, out Vector128 first1, out Vector128 first2, out Vector128 first3); - Av1IntraPredictorBase.Widen(second, out Vector128 second0, out Vector128 second1, out Vector128 second2, out Vector128 second3); - Av1IntraPredictorBase.Widen(alpha, out Vector128 alpha0, out Vector128 alpha1, out Vector128 alpha2, out Vector128 alpha3); - return Av1IntraPredictorBase.Narrow( + Av1NonDirectionalIntraPredictorBase.Widen(first, out Vector128 first0, out Vector128 first1, out Vector128 first2, out Vector128 first3); + Av1NonDirectionalIntraPredictorBase.Widen(second, out Vector128 second0, out Vector128 second1, out Vector128 second2, out Vector128 second3); + Av1NonDirectionalIntraPredictorBase.Widen(alpha, out Vector128 alpha0, out Vector128 alpha1, out Vector128 alpha2, out Vector128 alpha3); + return Av1NonDirectionalIntraPredictorBase.Narrow( Blend(first0, second0, alpha0), Blend(first1, second1, alpha1), Blend(first2, second2, alpha2), @@ -125,10 +125,10 @@ internal static partial class Av1CompoundMaskBlendPredictor [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 Blend(Vector256 first, Vector256 second, Vector256 alpha) { - Av1IntraPredictorBase.Widen(first, out Vector256 first0, out Vector256 first1, out Vector256 first2, out Vector256 first3); - Av1IntraPredictorBase.Widen(second, out Vector256 second0, out Vector256 second1, out Vector256 second2, out Vector256 second3); - Av1IntraPredictorBase.Widen(alpha, out Vector256 alpha0, out Vector256 alpha1, out Vector256 alpha2, out Vector256 alpha3); - return Av1IntraPredictorBase.Narrow( + Av1NonDirectionalIntraPredictorBase.Widen(first, out Vector256 first0, out Vector256 first1, out Vector256 first2, out Vector256 first3); + Av1NonDirectionalIntraPredictorBase.Widen(second, out Vector256 second0, out Vector256 second1, out Vector256 second2, out Vector256 second3); + Av1NonDirectionalIntraPredictorBase.Widen(alpha, out Vector256 alpha0, out Vector256 alpha1, out Vector256 alpha2, out Vector256 alpha3); + return Av1NonDirectionalIntraPredictorBase.Narrow( Blend(first0, second0, alpha0), Blend(first1, second1, alpha1), Blend(first2, second2, alpha2), @@ -139,10 +139,10 @@ internal static partial class Av1CompoundMaskBlendPredictor [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector512 Blend(Vector512 first, Vector512 second, Vector512 alpha) { - Av1IntraPredictorBase.Widen(first, out Vector512 first0, out Vector512 first1, out Vector512 first2, out Vector512 first3); - Av1IntraPredictorBase.Widen(second, out Vector512 second0, out Vector512 second1, out Vector512 second2, out Vector512 second3); - Av1IntraPredictorBase.Widen(alpha, out Vector512 alpha0, out Vector512 alpha1, out Vector512 alpha2, out Vector512 alpha3); - return Av1IntraPredictorBase.Narrow( + Av1NonDirectionalIntraPredictorBase.Widen(first, out Vector512 first0, out Vector512 first1, out Vector512 first2, out Vector512 first3); + Av1NonDirectionalIntraPredictorBase.Widen(second, out Vector512 second0, out Vector512 second1, out Vector512 second2, out Vector512 second3); + Av1NonDirectionalIntraPredictorBase.Widen(alpha, out Vector512 alpha0, out Vector512 alpha1, out Vector512 alpha2, out Vector512 alpha3); + return Av1NonDirectionalIntraPredictorBase.Narrow( Blend(first0, second0, alpha0), Blend(first1, second1, alpha1), Blend(first2, second2, alpha2), @@ -153,30 +153,30 @@ internal static partial class Av1CompoundMaskBlendPredictor [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector128 Blend(Vector128 first, Vector128 second, Vector128 alpha) { - Av1IntraPredictorBase.Widen(first.AsInt16(), out Vector128 first0, out Vector128 first1); - Av1IntraPredictorBase.Widen(second.AsInt16(), out Vector128 second0, out Vector128 second1); - Av1IntraPredictorBase.Widen(alpha.AsInt16(), out Vector128 alpha0, out Vector128 alpha1); - return Av1IntraPredictorBase.Narrow(Blend(first0, second0, alpha0), Blend(first1, second1, alpha1)).AsUInt16(); + Av1NonDirectionalIntraPredictorBase.Widen(first.AsInt16(), out Vector128 first0, out Vector128 first1); + Av1NonDirectionalIntraPredictorBase.Widen(second.AsInt16(), out Vector128 second0, out Vector128 second1); + Av1NonDirectionalIntraPredictorBase.Widen(alpha.AsInt16(), out Vector128 alpha0, out Vector128 alpha1); + return Av1NonDirectionalIntraPredictorBase.Narrow(Blend(first0, second0, alpha0), Blend(first1, second1, alpha1)).AsUInt16(); } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 Blend(Vector256 first, Vector256 second, Vector256 alpha) { - Av1IntraPredictorBase.Widen(first.AsInt16(), out Vector256 first0, out Vector256 first1); - Av1IntraPredictorBase.Widen(second.AsInt16(), out Vector256 second0, out Vector256 second1); - Av1IntraPredictorBase.Widen(alpha.AsInt16(), out Vector256 alpha0, out Vector256 alpha1); - return Av1IntraPredictorBase.Narrow(Blend(first0, second0, alpha0), Blend(first1, second1, alpha1)).AsUInt16(); + Av1NonDirectionalIntraPredictorBase.Widen(first.AsInt16(), out Vector256 first0, out Vector256 first1); + Av1NonDirectionalIntraPredictorBase.Widen(second.AsInt16(), out Vector256 second0, out Vector256 second1); + Av1NonDirectionalIntraPredictorBase.Widen(alpha.AsInt16(), out Vector256 alpha0, out Vector256 alpha1); + return Av1NonDirectionalIntraPredictorBase.Narrow(Blend(first0, second0, alpha0), Blend(first1, second1, alpha1)).AsUInt16(); } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector512 Blend(Vector512 first, Vector512 second, Vector512 alpha) { - Av1IntraPredictorBase.Widen(first.AsInt16(), out Vector512 first0, out Vector512 first1); - Av1IntraPredictorBase.Widen(second.AsInt16(), out Vector512 second0, out Vector512 second1); - Av1IntraPredictorBase.Widen(alpha.AsInt16(), out Vector512 alpha0, out Vector512 alpha1); - return Av1IntraPredictorBase.Narrow(Blend(first0, second0, alpha0), Blend(first1, second1, alpha1)).AsUInt16(); + Av1NonDirectionalIntraPredictorBase.Widen(first.AsInt16(), out Vector512 first0, out Vector512 first1); + Av1NonDirectionalIntraPredictorBase.Widen(second.AsInt16(), out Vector512 second0, out Vector512 second1); + Av1NonDirectionalIntraPredictorBase.Widen(alpha.AsInt16(), out Vector512 alpha0, out Vector512 alpha1); + return Av1NonDirectionalIntraPredictorBase.Narrow(Blend(first0, second0, alpha0), Blend(first1, second1, alpha1)).AsUInt16(); } /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundMaskBlendPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundMaskBlendPredictor.cs index 38ce40a50..c798d74c3 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundMaskBlendPredictor.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundMaskBlendPredictor.cs @@ -6,7 +6,7 @@ using System.Runtime.InteropServices; using System.Runtime.Intrinsics; 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1DifferenceWeightedMaskBuilder.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1DifferenceWeightedMaskBuilder.Operator.cs index 5143c2fa3..c8d18c273 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1DifferenceWeightedMaskBuilder.Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1DifferenceWeightedMaskBuilder.Operator.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1DifferenceWeightedMaskBuilder.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1DifferenceWeightedMaskBuilder.cs index f7339b6a1..1ebc07cbf 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1DifferenceWeightedMaskBuilder.cs +++ b/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 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterIntraMaskBuilder.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterIntraMaskBuilder.cs index c1c671d10..b1e8eaa90 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterIntraMaskBuilder.cs +++ b/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 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.Operator.cs index b4fa5aac5..13e1243a8 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.Operator.cs @@ -4,7 +4,7 @@ using System.Runtime.CompilerServices; 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; @@ -272,7 +272,7 @@ internal static partial class Av1ScaledInterPredictor Vector128 initial, out Vector128 result0, out Vector128 result1) - => Av1InterPredictor.Convolve( + => Av1TranslationalInterPredictor.Convolve( ref source, sourceStride, sourceOffset, @@ -293,7 +293,7 @@ internal static partial class Av1ScaledInterPredictor Vector256 initial, out Vector256 result0, out Vector256 result1) - => Av1InterPredictor.Convolve( + => Av1TranslationalInterPredictor.Convolve( ref source, sourceStride, sourceOffset, @@ -314,7 +314,7 @@ internal static partial class Av1ScaledInterPredictor Vector512 initial, out Vector512 result0, out Vector512 result1) - => Av1InterPredictor.Convolve( + => Av1TranslationalInterPredictor.Convolve( ref source, sourceStride, sourceOffset, @@ -426,7 +426,7 @@ internal static partial class Av1ScaledInterPredictor Vector128 result1, int bitDepth) where T : unmanaged - => Av1IntraPredictorBase.Narrow(result0, result1) + => Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1) .AsUInt16() .StoreUnsafe(ref Unsafe.As(ref destination), (nuint)index); @@ -439,7 +439,7 @@ internal static partial class Av1ScaledInterPredictor Vector256 result1, int bitDepth) where T : unmanaged - => Av1IntraPredictorBase.Narrow(result0, result1) + => Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1) .AsUInt16() .StoreUnsafe(ref Unsafe.As(ref destination), (nuint)index); @@ -452,7 +452,7 @@ internal static partial class Av1ScaledInterPredictor Vector512 result1, int bitDepth) where T : unmanaged - => Av1IntraPredictorBase.Narrow(result0, result1) + => Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1) .AsUInt16() .StoreUnsafe(ref Unsafe.As(ref destination), (nuint)index); } diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.cs index 4a8a40d2a..abc09f67e 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; 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; @@ -423,7 +423,7 @@ internal static partial class Av1ScaledInterPredictor horizontalBias, round0); - Av1IntraPredictorBase.Narrow(result, Vector512.Zero) + Av1NonDirectionalIntraPredictorBase.Narrow(result, Vector512.Zero) .GetLower() .StoreUnsafe(ref scratchRow, (nuint)column); } @@ -443,7 +443,7 @@ internal static partial class Av1ScaledInterPredictor horizontalBias, round0); - Av1IntraPredictorBase.Narrow(result, Vector256.Zero) + Av1NonDirectionalIntraPredictorBase.Narrow(result, Vector256.Zero) .GetLower() .StoreUnsafe(ref scratchRow, (nuint)column); } @@ -462,7 +462,7 @@ internal static partial class Av1ScaledInterPredictor horizontalBias, round0); - Av1IntraPredictorBase.Narrow(result, Vector128.Zero) + Av1NonDirectionalIntraPredictorBase.Narrow(result, Vector128.Zero) .GetLower() .StoreUnsafe(ref scratchRow, (nuint)column); } diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Arithmetic.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Arithmetic.cs similarity index 89% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Arithmetic.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Arithmetic.cs index 212cd91ef..cdf0d57f9 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Arithmetic.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Arithmetic.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; /// /// Provides lane-wise convolution, rounding, clipping, and packing shared by every interpolation filter. /// -internal static partial class Av1InterPredictor +internal static partial class Av1TranslationalInterPredictor { /// /// 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++) { Vector128 samples = Vector128.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column); - Av1IntraPredictorBase.Widen(samples, out Vector128 samples0, out Vector128 samples1, out Vector128 samples2, out Vector128 samples3); + Av1NonDirectionalIntraPredictorBase.Widen(samples, out Vector128 samples0, out Vector128 samples1, out Vector128 samples2, out Vector128 samples3); Vector128 coefficient = Vector128.Create((int)Unsafe.Add(ref coefficients, tap)); // 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++) { Vector256 samples = Vector256.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column); - Av1IntraPredictorBase.Widen(samples, out Vector256 samples0, out Vector256 samples1, out Vector256 samples2, out Vector256 samples3); + Av1NonDirectionalIntraPredictorBase.Widen(samples, out Vector256 samples0, out Vector256 samples1, out Vector256 samples2, out Vector256 samples3); Vector256 coefficient = Vector256.Create((int)Unsafe.Add(ref coefficients, tap)); result0 += samples0 * coefficient; @@ -105,7 +105,7 @@ internal static partial class Av1InterPredictor for (int tap = 0; tap < tapCount; tap++) { Vector512 samples = Vector512.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column); - Av1IntraPredictorBase.Widen(samples, out Vector512 samples0, out Vector512 samples1, out Vector512 samples2, out Vector512 samples3); + Av1NonDirectionalIntraPredictorBase.Widen(samples, out Vector512 samples0, out Vector512 samples1, out Vector512 samples2, out Vector512 samples3); Vector512 coefficient = Vector512.Create((int)Unsafe.Add(ref coefficients, tap)); result0 += samples0 * coefficient; @@ -135,7 +135,7 @@ internal static partial class Av1InterPredictor for (int tap = 0; tap < tapCount; tap++) { Vector128 samples = Vector128.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column); - Av1IntraPredictorBase.Widen(samples, out Vector128 samples0, out Vector128 samples1); + Av1NonDirectionalIntraPredictorBase.Widen(samples, out Vector128 samples0, out Vector128 samples1); Vector128 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 @@ -165,7 +165,7 @@ internal static partial class Av1InterPredictor for (int tap = 0; tap < tapCount; tap++) { Vector256 samples = Vector256.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column); - Av1IntraPredictorBase.Widen(samples, out Vector256 samples0, out Vector256 samples1); + Av1NonDirectionalIntraPredictorBase.Widen(samples, out Vector256 samples0, out Vector256 samples1); Vector256 coefficient = Vector256.Create((int)Unsafe.Add(ref coefficients, tap)); result0 += samples0 * coefficient; result1 += samples1 * coefficient; @@ -192,7 +192,7 @@ internal static partial class Av1InterPredictor for (int tap = 0; tap < tapCount; tap++) { Vector512 samples = Vector512.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column); - Av1IntraPredictorBase.Widen(samples, out Vector512 samples0, out Vector512 samples1); + Av1NonDirectionalIntraPredictorBase.Widen(samples, out Vector512 samples0, out Vector512 samples1); Vector512 coefficient = Vector512.Create((int)Unsafe.Add(ref coefficients, tap)); result0 += samples0 * coefficient; result1 += samples1 * coefficient; @@ -231,7 +231,7 @@ internal static partial class Av1InterPredictor result1 = Vector128.Clamp(result1, Vector128.Zero, maximum); result2 = Vector128.Clamp(result2, Vector128.Zero, maximum); result3 = Vector128.Clamp(result3, Vector128.Zero, maximum); - return Av1IntraPredictorBase.Narrow(result0, result1, result2, result3); + return Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1, result2, result3); } /// @@ -245,7 +245,7 @@ internal static partial class Av1InterPredictor result1 = Vector256.Clamp(result1, Vector256.Zero, maximum); result2 = Vector256.Clamp(result2, Vector256.Zero, maximum); result3 = Vector256.Clamp(result3, Vector256.Zero, maximum); - return Av1IntraPredictorBase.Narrow(result0, result1, result2, result3); + return Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1, result2, result3); } /// @@ -259,7 +259,7 @@ internal static partial class Av1InterPredictor result1 = Vector512.Clamp(result1, Vector512.Zero, maximum); result2 = Vector512.Clamp(result2, Vector512.Zero, maximum); result3 = Vector512.Clamp(result3, Vector512.Zero, maximum); - return Av1IntraPredictorBase.Narrow(result0, result1, result2, result3); + return Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1, result2, result3); } /// @@ -271,7 +271,7 @@ internal static partial class Av1InterPredictor Vector128 maximum = Vector128.Create(maximumValue); result0 = Vector128.Clamp(result0, Vector128.Zero, maximum); result1 = Vector128.Clamp(result1, Vector128.Zero, maximum); - return Av1IntraPredictorBase.Narrow(result0, result1).AsUInt16(); + return Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1).AsUInt16(); } /// @@ -283,7 +283,7 @@ internal static partial class Av1InterPredictor Vector256 maximum = Vector256.Create(maximumValue); result0 = Vector256.Clamp(result0, Vector256.Zero, maximum); result1 = Vector256.Clamp(result1, Vector256.Zero, maximum); - return Av1IntraPredictorBase.Narrow(result0, result1).AsUInt16(); + return Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1).AsUInt16(); } /// @@ -295,7 +295,7 @@ internal static partial class Av1InterPredictor Vector512 maximum = Vector512.Create(maximumValue); result0 = Vector512.Clamp(result0, Vector512.Zero, maximum); result1 = Vector512.Clamp(result1, Vector512.Zero, maximum); - return Av1IntraPredictorBase.Narrow(result0, result1).AsUInt16(); + return Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1).AsUInt16(); } /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.BilinearOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.BilinearOperator.cs similarity index 90% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.BilinearOperator.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.BilinearOperator.cs index 5d81a3a37..3cc1a0869 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.BilinearOperator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.BilinearOperator.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; /// /// Defines bilinear interpolation for translational inter prediction. /// -internal static partial class Av1InterPredictor +internal static partial class Av1TranslationalInterPredictor { /// /// Selects bilinear interpolation coefficients. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Dispatch.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Dispatch.cs similarity index 99% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Dispatch.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Dispatch.cs index 1f5afd1cb..edc5ac5fe 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Dispatch.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Dispatch.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; /// /// Selects interpolation filters and the widest supported traversal for a translational prediction block. /// -internal static partial class Av1InterPredictor +internal static partial class Av1TranslationalInterPredictor { /// /// Selects an 8-bit horizontal interpolation operator. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Filters.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Filters.cs similarity index 98% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Filters.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Filters.cs index 1e4c9e99c..9e919704d 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Filters.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Filters.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; /// /// Provides the normative Q7 interpolation coefficients used by AV1 inter prediction. /// -internal static partial class Av1InterPredictor +internal static partial class Av1TranslationalInterPredictor { /// /// The number of stored coefficient positions in every decoder interpolation kernel. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.OneDimension.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.OneDimension.cs similarity index 99% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.OneDimension.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.OneDimension.cs index 594aac772..bfa0d8d22 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.OneDimension.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.OneDimension.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; /// /// Provides SIMD kernels for horizontal-only and vertical-only single-reference filtering. /// -internal static partial class Av1InterPredictor +internal static partial class Av1TranslationalInterPredictor { /// /// Filters an 8-bit block in sixteen-sample vectors. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Operator.cs similarity index 99% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Operator.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Operator.cs index 17aa2e5f4..2aed6afbf 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Operator.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; /// /// Defines interpolation operators and the generic traversal used by translational inter prediction. /// -internal static partial class Av1InterPredictor +internal static partial class Av1TranslationalInterPredictor { /// /// Supplies the normative Q7 coefficient kernel for one AV1 interpolation-filter family. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.RegularOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.RegularOperator.cs similarity index 91% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.RegularOperator.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.RegularOperator.cs index 4eed3bad4..dfa24eb0a 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.RegularOperator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.RegularOperator.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; /// /// Defines regular interpolation for translational inter prediction. /// -internal static partial class Av1InterPredictor +internal static partial class Av1TranslationalInterPredictor { /// /// Selects regular interpolation coefficients. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.SharpOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.SharpOperator.cs similarity index 93% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.SharpOperator.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.SharpOperator.cs index b826a08a7..9c49a72cd 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.SharpOperator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.SharpOperator.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; /// /// Defines sharp interpolation for translational inter prediction. /// -internal static partial class Av1InterPredictor +internal static partial class Av1TranslationalInterPredictor { /// /// Selects sharp interpolation coefficients. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.SmoothOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.SmoothOperator.cs similarity index 91% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.SmoothOperator.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.SmoothOperator.cs index a3e883c6d..ae0517ef7 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.SmoothOperator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.SmoothOperator.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; /// /// Defines smooth interpolation for translational inter prediction. /// -internal static partial class Av1InterPredictor +internal static partial class Av1TranslationalInterPredictor { /// /// Selects smooth interpolation coefficients. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.TwoDimensions.Byte.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.TwoDimensions.Byte.cs similarity index 95% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.TwoDimensions.Byte.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.TwoDimensions.Byte.cs index 49f241da2..9ff9105a3 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.TwoDimensions.Byte.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.TwoDimensions.Byte.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; /// /// Provides separable SIMD convolution for 8-bit single-reference prediction. /// -internal static partial class Av1InterPredictor +internal static partial class Av1TranslationalInterPredictor { /// /// 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 result2, out Vector128 result3); - Av1IntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow); - Av1IntraPredictorBase.Narrow(RoundPowerOfTwo(result2, round0), RoundPowerOfTwo(result3, round0)).StoreUnsafe(ref scratchRow, (nuint)Vector128.Count); + Av1NonDirectionalIntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow); + Av1NonDirectionalIntraPredictorBase.Narrow(RoundPowerOfTwo(result2, round0), RoundPowerOfTwo(result3, round0)).StoreUnsafe(ref scratchRow, (nuint)Vector128.Count); continue; } @@ -83,8 +83,8 @@ internal static partial class Av1InterPredictor out Vector128 result2, out Vector128 result3); - Av1IntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); - Av1IntraPredictorBase.Narrow( + Av1NonDirectionalIntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); + Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(result2, round0), RoundPowerOfTwo(result3, round0)).StoreUnsafe( ref scratchRow, @@ -221,8 +221,8 @@ internal static partial class Av1InterPredictor out Vector256 result2, out Vector256 result3); - Av1IntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); - Av1IntraPredictorBase.Narrow( + Av1NonDirectionalIntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); + Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(result2, round0), RoundPowerOfTwo(result3, round0)).StoreUnsafe( ref scratchRow, @@ -328,8 +328,8 @@ internal static partial class Av1InterPredictor out Vector512 result2, out Vector512 result3); - Av1IntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); - Av1IntraPredictorBase.Narrow( + Av1NonDirectionalIntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); + Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(result2, round0), RoundPowerOfTwo(result3, round0)).StoreUnsafe( ref scratchRow, diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.TwoDimensions.UInt16.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.TwoDimensions.UInt16.cs similarity index 98% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.TwoDimensions.UInt16.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.TwoDimensions.UInt16.cs index 691a1c5d4..e136694a1 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.TwoDimensions.UInt16.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.TwoDimensions.UInt16.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; /// /// Provides separable SIMD convolution for unsigned 16-bit single-reference prediction. /// -internal static partial class Av1InterPredictor +internal static partial class Av1TranslationalInterPredictor { /// /// 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 result0, out Vector128 result1); - Av1IntraPredictorBase.Narrow( + Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow); @@ -87,7 +87,7 @@ internal static partial class Av1InterPredictor out Vector128 result0, out Vector128 result1); - Av1IntraPredictorBase.Narrow( + Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); } @@ -223,7 +223,7 @@ internal static partial class Av1InterPredictor out Vector256 result0, out Vector256 result1); - Av1IntraPredictorBase.Narrow( + Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); } @@ -337,7 +337,7 @@ internal static partial class Av1InterPredictor out Vector512 result0, out Vector512 result1); - Av1IntraPredictorBase.Narrow( + Av1NonDirectionalIntraPredictorBase.Narrow( RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); } diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.cs similarity index 99% rename from src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.cs rename to src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.cs index efd3f4fd8..2cfb40ecc 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterPredictor.cs +++ b/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. /// /// -internal static partial class Av1InterPredictor +internal static partial class Av1TranslationalInterPredictor { /// /// The number of fractional bits in each interpolation coefficient. diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.Filters.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.Filters.cs index da0a5cc45..9d6bc439f 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.Filters.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.Filters.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.Operator.cs index 4a882d843..c148a36a9 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.Operator.cs @@ -4,7 +4,7 @@ using System.Runtime.CompilerServices; 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.cs index 5d3aa45b5..04a52d1a4 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.cs @@ -6,7 +6,7 @@ using System.Runtime.InteropServices; using System.Runtime.Intrinsics; 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; diff --git a/src/ImageSharp/Formats/Heif/Av1/Readme.md b/src/ImageSharp/Formats/Heif/Av1/Readme.md index 9f658e90c..cc18ba1e0 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Readme.md +++ b/src/ImageSharp/Formats/Heif/Av1/Readme.md @@ -1,27 +1,26 @@ # Open Bitstream Unit -An OBU unit is a unit of parameters encoded in a bitstream format. In AVIF, it contains a single frame. -This frame is coded using no other frame as reference, it is a so called INTRA frame. AV1 movie encoding also defines 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 is a unit of syntax encoded in an AV1 bitstream. HEIF image items can contain still-picture, +progressive, or dependent-frame AV1 payloads, so the decoder handles both intra and inter frames. An OBU section for AVIF consists of the following headers: ## 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 -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 setting is recommended, as all the extra parameters are not applicable for AVIF. +This is the common header for a sequence of frames. A still picture can use the reduced syntax selected by +`ReducedStillPictureHeader`; progressive and dependent-frame payloads use the complete sequence syntax. ## 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 -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 @@ -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. 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 -A superblock contains one or more Partitions. The partition Type determines the number of partitions it is further split in. -Paritions can contain other partitions and blocks. +A superblock contains one or more partitions. The partition type determines how the area is split. +Partitions can contain other partitions and blocks. ## 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 @@ -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) -[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) @@ -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/) [AVIF sample images](https://github.com/link-u/avif-sample-images) - diff --git a/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrame.cs b/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrame.cs index ed43d164e..b90457ea2 100644 --- a/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrame.cs +++ b/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrame.cs @@ -22,23 +22,28 @@ internal sealed class Av1ReferenceFrame : IDisposable private Av1FrameBuffer? frameBuffer; /// - /// 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. /// - private Av1FrameEntropyContext? entropyContext; + private ReferenceOwnership? referenceOwnership; /// - /// The decoder-session owner that receives when this frame is released. + /// Initializes a new instance of the class for presentation-only ownership. /// - private Av1FrameEntropyContexts? entropyContextOwner; - - /// - /// The shared decoded per-block state while this frame owns one lifetime lease. - /// - private Av1FrameInfo? frameInfo; + /// + /// The completed sample buffer. Ownership transfers to this instance when construction succeeds. + /// + /// + /// The completed frame header associated with the reconstructed samples. The caller must not mutate the header + /// after transferring it to this instance. + /// + public Av1ReferenceFrame(Av1FrameBuffer frameBuffer, ObuFrameHeader frameHeader) + { + this.frameBuffer = frameBuffer; + this.FrameHeader = frameHeader; + } /// - /// Initializes a new instance of the class and takes ownership of the decoded - /// sample buffer. + /// Initializes a new instance of the class with retained compact reference state. /// /// /// 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 /// after transferring it to this instance. /// - /// - /// The completed per-block state associated with the reconstructed samples. The caller must not mutate the state - /// after transferring it to this instance. - /// + /// The completed reconstruction state from which reference syntax is retained. public Av1ReferenceFrame(Av1FrameBuffer frameBuffer, ObuFrameHeader frameHeader, Av1FrameInfo frameInfo) + : this(frameBuffer, frameHeader) { - this.frameBuffer = frameBuffer; - this.FrameHeader = frameHeader; - this.frameInfo = frameInfo; - frameInfo.AddOwner(); + this.referenceOwnership = new(frameInfo.AcquireReferenceState(), null); } /// @@ -82,16 +82,24 @@ internal sealed class Av1ReferenceFrame : IDisposable Av1FrameInfo frameInfo, Av1FrameEntropyContext entropyContext, Av1FrameEntropyContexts entropyContextOwner) - : this(frameBuffer, frameHeader, frameInfo) + : this(frameBuffer, frameHeader) { - this.entropyContext = entropyContext; - this.entropyContextOwner = entropyContextOwner; + this.referenceOwnership = new( + frameInfo.AcquireReferenceState(), + new EntropyOwnership(entropyContext, entropyContextOwner)); } /// /// Gets the completed sample buffer owned by this frame. /// - public Av1FrameBuffer FrameBuffer => this.frameBuffer!; + public Av1FrameBuffer FrameBuffer + { + get + { + return this.frameBuffer + ?? throw new ObjectDisposedException(nameof(Av1ReferenceFrame)); + } + } /// /// Gets the completed header that describes the retained frame. @@ -99,21 +107,41 @@ internal sealed class Av1ReferenceFrame : IDisposable public ObuFrameHeader FrameHeader { get; } /// - /// 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. /// - 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; + } + } /// /// Gets the entropy context retained for primary-reference use, or for a presentation-only /// frame. /// - public Av1FrameEntropyContext? EntropyContext => this.entropyContext; + public Av1FrameEntropyContext? EntropyContext => this.referenceOwnership?.Entropy?.Context; /// /// Restores the retained frame context to the normative defaults selected by this frame's quantizer band. /// 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); + } /// /// Transfers the completed sample planes out of this frame owner. @@ -121,7 +149,9 @@ internal sealed class Av1ReferenceFrame : IDisposable /// The completed sample planes now owned by the caller. public Av1FrameBuffer TakeFrameBuffer() { - Av1FrameBuffer result = this.frameBuffer!; + Av1FrameBuffer result = this.frameBuffer + ?? throw new ObjectDisposedException(nameof(Av1ReferenceFrame)); + this.frameBuffer = null; return result; } @@ -131,19 +161,60 @@ internal sealed class Av1ReferenceFrame : IDisposable /// public void Dispose() { - Av1FrameEntropyContext? context = this.entropyContext; - this.entropyContext = null; - if (context is not null) + ReferenceOwnership? ownership = this.referenceOwnership; + this.referenceOwnership = null; + if (ownership is not null) { - // Nulling the field before returning the graph makes repeated disposal harmless and guarantees that one - // shared frame owner occupying multiple reference slots returns its snapshot exactly once. - this.entropyContextOwner!.ReturnSnapshot(context); - this.entropyContextOwner = null; + // Clearing the complete ownership state before returning either resource makes repeated disposal harmless + // when one frame owner occupies multiple reference-map slots. + ReferenceOwnership activeOwnership = ownership.Value; + 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 = null; - this.frameInfo?.ReleaseOwner(); - this.frameInfo = null; + } + + /// + /// Carries the complete state retained only by frames that can be selected as references. + /// + private readonly struct ReferenceOwnership( + Av1FrameInfo.ReferenceState referenceState, + EntropyOwnership? entropy) + { + /// + /// Gets the retained segment and motion state. + /// + public Av1FrameInfo.ReferenceState ReferenceState { get; } = referenceState; + + /// + /// Gets the retained entropy snapshot and its return owner when one was published. + /// + public EntropyOwnership? Entropy { get; } = entropy; + } + + /// + /// Pairs a retained entropy snapshot with the decoder-session owner that must receive it on release. + /// + private readonly struct EntropyOwnership( + Av1FrameEntropyContext context, + Av1FrameEntropyContexts owner) + { + /// + /// Gets the retained entropy snapshot. + /// + public Av1FrameEntropyContext Context { get; } = context; + + /// + /// Gets the decoder-session owner that receives the snapshot. + /// + public Av1FrameEntropyContexts Owner { get; } = owner; } } diff --git a/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameBorder.cs b/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameBorder.cs index d3912f44b..c8cbf5712 100644 --- a/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameBorder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameBorder.cs @@ -24,7 +24,7 @@ internal static class Av1ReferenceFrameBorder ExtendPlane( frameBuffer, - frameBuffer.BufferY!, + frameBuffer.GetPlaneBuffer(Av1Plane.Y), frameBuffer.OriginX, frameBuffer.OriginY, frameBuffer.Width, @@ -37,8 +37,8 @@ internal static class Av1ReferenceFrameBorder int chromaOriginX = frameBuffer.OriginX >> subsamplingX; int chromaOriginY = frameBuffer.OriginY >> subsamplingY; - ExtendPlane(frameBuffer, frameBuffer.BufferCb!, chromaOriginX, chromaOriginY, chromaWidth, chromaHeight); - ExtendPlane(frameBuffer, frameBuffer.BufferCr!, chromaOriginX, chromaOriginY, chromaWidth, chromaHeight); + ExtendPlane(frameBuffer, frameBuffer.GetPlaneBuffer(Av1Plane.U), chromaOriginX, chromaOriginY, chromaWidth, chromaHeight); + ExtendPlane(frameBuffer, frameBuffer.GetPlaneBuffer(Av1Plane.V), chromaOriginX, chromaOriginY, chromaWidth, chromaHeight); } } diff --git a/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameStore.cs b/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameStore.cs index a900f54e0..82192dbad 100644 --- a/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameStore.cs +++ b/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameStore.cs @@ -42,6 +42,27 @@ internal sealed class Av1ReferenceFrameStore : IDisposable /// The retained frame, or when the slot has not been populated. public Av1ReferenceFrame? Resolve(int slot) => this.frames[slot]; + /// + /// Resolves a reference-map slot that an earlier syntax boundary has established as occupied. + /// + /// The zero-based reference-map slot. + /// The retained frame in the selected slot. + public Av1ReferenceFrame ResolveRequired(int slot) + { + return this.Resolve(slot) + ?? throw new InvalidImageContentException($"The AV1 reference-map slot {slot} has not been populated."); + } + + /// + /// Resolves the presentation output established by the completed bounded payload. + /// + /// The retained frame selected for presentation. + public Av1ReferenceFrame ResolveOutput() + { + return this.outputFrame + ?? throw new InvalidImageContentException("The AV1 payload did not produce a shown frame."); + } + /// /// Writes whether each reference-map slot currently owns a reconstructed frame. /// @@ -164,7 +185,7 @@ internal sealed class Av1ReferenceFrameStore : IDisposable /// The retained frame selected for presentation. public Av1ReferenceFrame ShowExisting(int slot) { - Av1ReferenceFrame selectedFrame = this.frames[slot]!; + Av1ReferenceFrame selectedFrame = this.ResolveRequired(slot); Av1ReferenceFrame? replacedOutputFrame = this.outputFrame; this.outputFrame = selectedFrame; @@ -217,7 +238,7 @@ internal sealed class Av1ReferenceFrameStore : IDisposable /// The selected presentation frame now owned by the caller. public Av1ReferenceFrame TakeOutput() { - Av1ReferenceFrame result = this.outputFrame!; + Av1ReferenceFrame result = this.ResolveOutput(); this.outputFrame = null; // The caller becomes the sole owner of the selected output. Remove all slot aliases before Reset releases the diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs index 2b6b0f48e..f12492b3b 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs @@ -58,12 +58,12 @@ internal struct Av1BlockModeInfo /// /// Stores the luma palette color-index map. /// - private Buffer2DRegion lumaPaletteColorIndexMap; + private Rectangle lumaPaletteColorIndexBounds; /// /// Stores the shared chroma palette color-index map. /// - private Buffer2DRegion chromaPaletteColorIndexMap; + private Rectangle chromaPaletteColorIndexBounds; /// /// The directional prediction angle adjustment for luma. @@ -440,24 +440,27 @@ internal struct Av1BlockModeInfo /// Gets the palette color-index map for a color plane. /// /// The color plane. + /// The decoder-session palette map for the selected plane class. /// The luma map for or the shared chroma map for either chroma plane. - public Buffer2DRegion GetPaletteColorIndexMap(Av1Plane plane) - => plane == Av1Plane.Y ? this.lumaPaletteColorIndexMap : this.chromaPaletteColorIndexMap; + public Buffer2DRegion GetPaletteColorIndexMap(Av1Plane plane, Buffer2D colorIndexMap) + => new( + colorIndexMap, + plane == Av1Plane.Y ? this.lumaPaletteColorIndexBounds : this.chromaPaletteColorIndexBounds); /// /// Stores the palette color-index map for a plane class. /// /// The luma or shared chroma plane class. - /// The row-major color-index map including coded-block edge padding. - public void SetPaletteColorIndexMap(Av1PlaneType planeType, Buffer2DRegion colorIndexMap) + /// The row-major color-index bounds including coded-block edge padding. + public void SetPaletteColorIndexMap(Av1PlaneType planeType, Rectangle bounds) { if (planeType == Av1PlaneType.Y) { - this.lumaPaletteColorIndexMap = colorIndexMap; + this.lumaPaletteColorIndexBounds = bounds; } else { - this.chromaPaletteColorIndexMap = colorIndexMap; + this.chromaPaletteColorIndexBounds = bounds; } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.MotionField.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.MotionField.cs index fbd64de86..37d55bc3d 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.MotionField.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.MotionField.cs @@ -52,15 +52,20 @@ internal partial class Av1FrameInfo /// /// Owns the selected motion vector and logical reference for every retained 8x8 frame position. /// - private IMemoryOwner? retainedMotionField; + private MotionFieldStorage? retainedMotionField; /// /// Owns motion vectors projected from retained frames into the current frame's 8x8 grid. /// - private IMemoryOwner? temporalMotionField; + private MotionFieldStorage? temporalMotionField; /// - /// 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. + /// + private ReferenceState? referenceState; + + /// + /// The number of tile-reader and decoder-result owners retaining this reconstruction state. /// private int ownerCount = 1; @@ -84,16 +89,6 @@ internal partial class Av1FrameInfo /// private uint motionFieldOrderHint; - /// - /// The number of retained motion-field entries in one active 8x8 row. - /// - private int retainedMotionFieldStride; - - /// - /// The number of projected temporal-motion entries in one aligned 8x8 row. - /// - private int temporalMotionFieldStride; - /// /// The active frame width in 4x4 mode-information units. /// @@ -125,7 +120,7 @@ internal partial class Av1FrameInfo this.motionFieldOrderHint = frameHeader.OrderHint; this.activeModeInfoColumnCount = frameHeader.ModeInfoColumnCount; this.activeModeInfoRowCount = frameHeader.ModeInfoRowCount; - this.retainedMotionFieldStride = (this.activeModeInfoColumnCount + 1) >> MotionFieldModeInfoShift; + int retainedMotionFieldStride = (this.activeModeInfoColumnCount + 1) >> MotionFieldModeInfoShift; if (frameHeader.IsIntra) { @@ -134,8 +129,8 @@ internal partial class Av1FrameInfo return; } - InlineArray8 selectedReferences = default; ReadOnlySpan referenceFrameIndices = frameHeader.GetReferenceFrameIndices(); + SelectedReferenceFrames selectedReferences = new(referenceFrames, referenceFrameIndices); ObuOrderHintInfo orderHintInfo = sequenceHeader.OrderHintInfo; // 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++) { Av1ReferenceFrameType referenceFrameType = (Av1ReferenceFrameType)(referenceIndex + 1); - Av1ReferenceFrame referenceFrame = referenceFrames.Resolve((int)referenceFrameIndices[referenceIndex])!; + Av1ReferenceFrame referenceFrame = selectedReferences[referenceFrameType]; uint referenceOrderHint = referenceFrame.FrameHeader.OrderHint; - selectedReferences[(int)referenceFrameType] = referenceFrame; this.motionFieldReferenceOrderHints[(int)referenceFrameType] = referenceOrderHint; int relativeDistance = orderHintInfo.GetRelativeDistance(referenceOrderHint, frameHeader.OrderHint); @@ -161,13 +155,16 @@ internal partial class Av1FrameInfo return; } - // FrameInfo is shared directly by the tile reader, reference frames, and the final decoder result. The - // allocator-owned motion fields therefore follow that shared lifetime without placing frame-sized arrays on - // the managed heap. Clean storage is required because an all-zero entry denotes the normative empty field. + // The retained field transfers to compact reference state after reconstruction. Keeping it allocator-backed + // avoids placing a frame-sized array on the managed heap. Clean storage is required because an all-zero entry + // denotes the normative empty field. int retainedRowCount = (this.activeModeInfoRowCount + 1) >> MotionFieldModeInfoShift; - this.retainedMotionField = configuration.MemoryAllocator.Allocate( - this.retainedMotionFieldStride * retainedRowCount, - AllocationOptions.Clean); + IMemoryOwner retainedMotionFieldOwner = + configuration.MemoryAllocator.Allocate( + retainedMotionFieldStride * retainedRowCount, + AllocationOptions.Clean); + + this.retainedMotionField = new(retainedMotionFieldOwner, retainedMotionFieldStride); if (!frameHeader.UseReferenceFrameMotionVectors) { @@ -180,57 +177,64 @@ internal partial class Av1FrameInfo this.activeModeInfoColumnCount, MaximumSuperblockModeInfoSizeLog2); - this.temporalMotionFieldStride = alignedModeInfoColumnCount >> MotionFieldModeInfoShift; + int temporalMotionFieldStride = alignedModeInfoColumnCount >> MotionFieldModeInfoShift; int temporalRowCount = (this.activeModeInfoRowCount + MaximumSuperblockModeInfoSize) >> MotionFieldModeInfoShift; - this.temporalMotionField = configuration.MemoryAllocator.Allocate( - this.temporalMotionFieldStride * temporalRowCount, - AllocationOptions.Clean); + IMemoryOwner temporalMotionFieldOwner = + configuration.MemoryAllocator.Allocate( + temporalMotionFieldStride * temporalRowCount, + AllocationOptions.Clean); + + MotionFieldStorage temporalMotionField = + new(temporalMotionFieldOwner, temporalMotionFieldStride); + + this.temporalMotionField = temporalMotionField; // 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 // when eligible projection succeeds, and LAST2 fills the final unused position in the reverse direction. int remainingProjectionCount = MotionFieldProjectionCount; - Av1ReferenceFrame lastFrame = selectedReferences[(int)Av1ReferenceFrameType.Last]!; - Av1ReferenceFrame goldenFrame = selectedReferences[(int)Av1ReferenceFrameType.Golden]!; - uint alternateOfLastOrderHint = lastFrame.FrameInfo.motionFieldReferenceOrderHints[(int)Av1ReferenceFrameType.Alternate]; + Av1ReferenceFrame lastFrame = selectedReferences[Av1ReferenceFrameType.Last]; + Av1ReferenceFrame goldenFrame = selectedReferences[Av1ReferenceFrameType.Golden]; + 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 // temporal source, but the reference decoder still consumes one position from the three-source projection budget. if (alternateOfLastOrderHint != goldenFrame.FrameHeader.OrderHint) { - _ = this.ProjectMotionField(sequenceHeader, frameHeader, lastFrame, reverseDirection: true); + _ = this.ProjectMotionField(sequenceHeader, frameHeader, lastFrame, temporalMotionField, reverseDirection: true); } remainingProjectionCount--; - Av1ReferenceFrame backwardFrame = selectedReferences[(int)Av1ReferenceFrameType.Backward]!; + Av1ReferenceFrame backwardFrame = selectedReferences[Av1ReferenceFrameType.Backward]; 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--; } - Av1ReferenceFrame alternate2Frame = selectedReferences[(int)Av1ReferenceFrameType.Alternate2]!; + Av1ReferenceFrame alternate2Frame = selectedReferences[Av1ReferenceFrameType.Alternate2]; 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--; } - Av1ReferenceFrame alternateFrame = selectedReferences[(int)Av1ReferenceFrameType.Alternate]!; + Av1ReferenceFrame alternateFrame = selectedReferences[Av1ReferenceFrameType.Alternate]; if (remainingProjectionCount > 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--; } if (remainingProjectionCount > 0) { - Av1ReferenceFrame last2Frame = selectedReferences[(int)Av1ReferenceFrameType.Last2]!; - _ = this.ProjectMotionField(sequenceHeader, frameHeader, last2Frame, reverseDirection: true); + Av1ReferenceFrame last2Frame = selectedReferences[Av1ReferenceFrameType.Last2]; + _ = this.ProjectMotionField(sequenceHeader, frameHeader, last2Frame, temporalMotionField, reverseDirection: true); } } @@ -248,10 +252,19 @@ internal partial class Av1FrameInfo out Av1MotionVector motionVector, out int referenceFrameOffset) { - int index = ((modeInfoRow >> MotionFieldModeInfoShift) * this.temporalMotionFieldStride) + + MotionFieldStorage? temporalMotionFieldState = this.temporalMotionField; + if (temporalMotionFieldState is null) + { + motionVector = default; + referenceFrameOffset = 0; + return false; + } + + MotionFieldStorage temporalMotionField = temporalMotionFieldState.Value; + int index = ((modeInfoRow >> MotionFieldModeInfoShift) * temporalMotionField.Stride) + (modeInfoColumn >> MotionFieldModeInfoShift); - TemporalMotionFieldEntry entry = this.temporalMotionField!.Memory.Span[index]; + TemporalMotionFieldEntry entry = temporalMotionField.Owner.Memory.Span[index]; motionVector = entry.MotionVector; referenceFrameOffset = entry.ReferenceFrameOffset; return referenceFrameOffset > 0; @@ -314,13 +327,14 @@ internal partial class Av1FrameInfo /// The block origin in frame-relative 4x4 units. private void UpdateRetainedMotionField(Av1BlockModeInfo modeInfo, Point modeInfoPosition) { - IMemoryOwner? retainedMotionField = this.retainedMotionField; - if (retainedMotionField is null) + MotionFieldStorage? retainedMotionFieldState = this.retainedMotionField; + if (retainedMotionFieldState is null) { return; } - Span retainedEntries = retainedMotionField.Memory.Span; + MotionFieldStorage retainedMotionField = retainedMotionFieldState.Value; + Span retainedEntries = retainedMotionField.Owner.Memory.Span; Av1ReferenceFrameType selectedReference = Av1ReferenceFrameType.None; Av1MotionVector selectedMotionVector = default; @@ -363,7 +377,7 @@ internal partial class Av1FrameInfo // normative ability to overwrite the shared cell in traversal order. 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); } } @@ -374,6 +388,7 @@ internal partial class Av1FrameInfo /// The sequence header defining the modulo order-hint domain. /// The current frame header. /// The retained frame whose stored motion vectors are projected. + /// The complete destination field storage for the current frame. /// /// A value indicating whether the start-to-current distance and spatial displacement are reversed for a past frame. /// @@ -382,6 +397,7 @@ internal partial class Av1FrameInfo ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, Av1ReferenceFrame startFrame, + MotionFieldStorage temporalMotionField, bool reverseDirection) { ObuFrameHeader startFrameHeader = startFrame.FrameHeader; @@ -394,7 +410,7 @@ internal partial class Av1FrameInfo return false; } - Av1FrameInfo startFrameInfo = startFrame.FrameInfo; + ReferenceState startFrameState = startFrame.ReferenceState; ObuOrderHintInfo orderHintInfo = sequenceHeader.OrderHintInfo; int startToCurrentFrameOffset = orderHintInfo.GetRelativeDistance( startFrameHeader.OrderHint, @@ -409,12 +425,20 @@ internal partial class Av1FrameInfo int sourceColumnCount = (this.activeModeInfoColumnCount + 1) >> MotionFieldModeInfoShift; int destinationRowCount = this.activeModeInfoRowCount >> MotionFieldModeInfoShift; int destinationColumnCount = this.activeModeInfoColumnCount >> MotionFieldModeInfoShift; - ReadOnlySpan sourceEntries = startFrameInfo.retainedMotionField!.Memory.Span; - Span destinationEntries = this.temporalMotionField!.Memory.Span; + MotionFieldStorage? retainedMotionFieldState = startFrameState.RetainedMotionField; + if (retainedMotionFieldState is null) + { + return false; + } + + MotionFieldStorage retainedMotionField = retainedMotionFieldState.Value; + + ReadOnlySpan sourceEntries = retainedMotionField.Owner.Memory.Span; + Span destinationEntries = temporalMotionField.Owner.Memory.Span; for (int blockRow = 0; blockRow < sourceRowCount; blockRow++) { - int sourceRowOffset = blockRow * startFrameInfo.retainedMotionFieldStride; + int sourceRowOffset = blockRow * retainedMotionField.Stride; for (int blockColumn = 0; blockColumn < sourceColumnCount; blockColumn++) { RetainedMotionFieldEntry source = sourceEntries[sourceRowOffset + blockColumn]; @@ -425,7 +449,7 @@ internal partial class Av1FrameInfo int referenceFrameOffset = orderHintInfo.GetRelativeDistance( startFrameHeader.OrderHint, - startFrameInfo.motionFieldReferenceOrderHints[(int)source.ReferenceFrame]); + startFrameState.MotionFieldReferenceOrderHints[(int)source.ReferenceFrame]); bool positionIsValid = Math.Abs(referenceFrameOffset) <= Av1MotionVector.MaximumTemporalDistance && 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 // 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); } } @@ -461,6 +485,43 @@ internal partial class Av1FrameInfo return true; } + /// + /// Detaches the compact state required while this decoded frame occupies the reference map. + /// + 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; + } + + /// + /// Acquires the prepared compact state for one reference-frame owner. + /// + /// The retained reference state with one ownership lease for the caller. + public ReferenceState AcquireReferenceState() + { + ReferenceState state = this.PrepareReferenceState(); + + state.AddOwner(); + return state; + } + /// /// Adds one owner for this frame state. /// @@ -493,12 +554,29 @@ internal partial class Av1FrameInfo this.ownerCount--; if (this.ownerCount == 0) { - // Frame-sized motion storage remains addressable through retained references. Return it only after tile, - // reference, presentation, and decoder-result owners are gone. + // Reconstruction-only syntax expires with the tile reader and optional decoder-result owner. A detached + // reference state has its own lease and can outlive this full frame state. + this.referenceState?.ReleaseOwner(); + this.referenceState = null; this.retainedMotionField?.Dispose(); this.retainedMotionField = null; this.temporalMotionField?.Dispose(); 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; } + /// + /// Couples one allocator-owned motion-field buffer with the stride required to address it. + /// + internal readonly struct MotionFieldStorage + where T : struct + { + /// + /// Initializes a new instance of the struct. + /// + public MotionFieldStorage(IMemoryOwner owner, int stride) + { + this.Owner = owner; + this.Stride = stride; + } + + /// + /// Gets the allocator-owned field entries. + /// + public IMemoryOwner Owner { get; } + + /// + /// Gets the number of entries in one field row. + /// + public int Stride { get; } + + /// + /// Returns the field entries to their allocator. + /// + public void Dispose() => this.Owner.Dispose(); + } + + /// + /// Carries the complete seven-role reference mapping captured before the current frame refreshes map slots. + /// + private readonly struct SelectedReferenceFrames + { + /// + /// Initializes a new instance of the struct. + /// + public SelectedReferenceFrames(Av1ReferenceFrameStore referenceFrames, ReadOnlySpan 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]); + } + + /// + /// Gets the retained LAST frame. + /// + public Av1ReferenceFrame Last { get; } + + /// + /// Gets the retained LAST2 frame. + /// + public Av1ReferenceFrame Last2 { get; } + + /// + /// Gets the retained LAST3 frame. + /// + public Av1ReferenceFrame Last3 { get; } + + /// + /// Gets the retained GOLDEN frame. + /// + public Av1ReferenceFrame Golden { get; } + + /// + /// Gets the retained BWDREF frame. + /// + public Av1ReferenceFrame Backward { get; } + + /// + /// Gets the retained ALTREF2 frame. + /// + public Av1ReferenceFrame Alternate2 { get; } + + /// + /// Gets the retained ALTREF frame. + /// + public Av1ReferenceFrame Alternate { get; } + + /// + /// Gets the retained frame for one canonical inter-reference role. + /// + 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.") + }; + } + /// /// Stores one motion vector and logical reference retained for projection by a later frame. /// - private readonly struct RetainedMotionFieldEntry + internal readonly struct RetainedMotionFieldEntry { /// /// Initializes a new instance of the struct. @@ -600,4 +779,77 @@ internal partial class Av1FrameInfo /// public int ReferenceFrameOffset { get; } } + + /// + /// Owns only the per-frame syntax retained by libaom's reference buffer after reconstruction completes. + /// + internal sealed class ReferenceState + { + /// + /// The number of frame-info and reference-frame owners retaining this state. + /// + private int ownerCount = 1; + + /// + /// Initializes a new instance of the class by taking ownership of retained buffers. + /// + public ReferenceState( + Buffer2D? segmentIds, + int segmentIdColumnCount, + int segmentIdRowCount, + MotionFieldStorage? retainedMotionField, + InlineArray8 motionFieldReferenceOrderHints) + { + this.SegmentIds = segmentIds; + this.SegmentIdColumnCount = segmentIdColumnCount; + this.SegmentIdRowCount = segmentIdRowCount; + this.RetainedMotionField = retainedMotionField; + this.MotionFieldReferenceOrderHints = motionFieldReferenceOrderHints; + } + + /// + /// Gets the retained 4x4 segmentation map, or when segmentation is disabled. + /// + public Buffer2D? SegmentIds { get; private set; } + + /// + /// Gets the number of active 4x4 columns in . + /// + public int SegmentIdColumnCount { get; } + + /// + /// Gets the number of active 4x4 rows in . + /// + public int SegmentIdRowCount { get; } + + /// + /// Gets the retained per-8x8 motion field, or when the temporal tool is disabled. + /// + public MotionFieldStorage? RetainedMotionField { get; private set; } + + /// + /// Gets the order hints selected by this frame's seven logical inter-reference roles. + /// + public InlineArray8 MotionFieldReferenceOrderHints { get; } + + /// + /// Adds one owner for this retained state. + /// + public void AddOwner() => this.ownerCount++; + + /// + /// Releases one owner and returns retained buffers after the final lease. + /// + public void ReleaseOwner() + { + this.ownerCount--; + if (this.ownerCount == 0) + { + this.RetainedMotionField?.Dispose(); + this.RetainedMotionField = null; + this.SegmentIds?.Dispose(); + this.SegmentIds = null; + } + } + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs index 22c776757..2c85573f7 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs @@ -1,35 +1,106 @@ // Copyright (c) Six Labors. // 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.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +/// +/// Identifies the inter-prediction features selected by decoded AV1 block syntax. +/// +[Flags] +internal enum Av1InterPredictionFeatures +{ + /// + /// No tracked inter-prediction feature was selected. + /// + None = 0, + + /// + /// Distance-weighted compound prediction was selected. + /// + DistanceWeightedCompound = 1 << 0, + + /// + /// A non-inverted wedge compound mask was selected. + /// + WedgeCompound = 1 << 1, + + /// + /// An inverted wedge compound mask was selected. + /// + InvertedWedgeCompound = 1 << 2, + + /// + /// The first difference-weighted compound mask orientation was selected. + /// + DifferenceWeightedCompound = 1 << 3, + + /// + /// The inverted difference-weighted compound mask orientation was selected. + /// + InvertedDifferenceWeightedCompound = 1 << 4, + + /// + /// Smooth inter-intra prediction was selected. + /// + SmoothInterIntra = 1 << 5, + + /// + /// Wedge inter-intra prediction was selected. + /// + WedgeInterIntra = 1 << 6, + + /// + /// Overlapping motion compensation was selected. + /// + Obmc = 1 << 7, + + /// + /// Local warped-motion prediction was selected. + /// + LocalWarp = 1 << 8, + + /// + /// Non-translational global warped-motion prediction was selected. + /// + GlobalWarp = 1 << 9 +} + /// /// Owns the mode, motion, segmentation, transform, coefficient, quantizer, and filter state decoded for one AV1 frame. /// -internal partial class Av1FrameInfo : IDisposable +internal sealed partial class Av1FrameInfo : IDisposable { + /// + /// The allocator that owns frame-sized syntax and retained-reference state. + /// + private readonly MemoryAllocator memoryAllocator; + /// /// The coefficient slots reserved for one 4x4 mode-information unit: one end index followed by 16 coefficients. /// public const int CoefficientCountPerModeInfo = 1 + 16; /// - /// Stores raster-ordered luma coefficients for every frame superblock. + /// Owns the luma and chroma coefficient scratch for the superblock currently being decoded. /// - private readonly int[] coefficientsY = []; + private readonly IMemoryOwner coefficientScratch; /// - /// Stores raster-ordered blue-difference chroma coefficients for every frame superblock. + /// The number of luma coefficient entries at the start of . /// - private readonly int[] coefficientsU = []; + private readonly int lumaCoefficientCount; /// - /// Stores raster-ordered red-difference chroma coefficients for every frame superblock. + /// The number of coefficient entries reserved for each chroma plane. /// - private readonly int[] coefficientsV = []; + private readonly int chromaCoefficientCount; /// /// 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; /// - /// Stores one addressing view for each frame superblock. + /// Stores decoded block mode information in bitstream traversal order. /// - private readonly Av1SuperblockInfo[] superblockInfos; + private readonly MemoryGroup modeInfos; /// - /// Stores decoded block mode information in bitstream traversal order. + /// Stores the number of mode-information records written to each superblock row in . /// - private readonly Av1BlockModeInfo[] modeInfos; + private readonly Buffer2D modeInfoCounts; /// /// Maps every frame-relative 4x4 position to its covering entry in . @@ -74,7 +145,7 @@ internal partial class Av1FrameInfo : IDisposable /// /// Stores the decoded segment identifier for each active 4x4 mode-information position in row-major order. /// - private byte[] segmentIds = []; + private Buffer2D? segmentIds; /// /// The number of active 4x4 columns in one row of . @@ -87,19 +158,14 @@ internal partial class Av1FrameInfo : IDisposable private int segmentIdRowCount; /// - /// Stores luma transform information grouped by superblock. - /// - private readonly Av1TransformInfo[] transformInfosY; - - /// - /// Stores both chroma planes' transform information grouped by superblock. + /// Owns the luma and shared-chroma transform-information scratch for the superblock currently being decoded. /// - private readonly Av1TransformInfo[] transformInfosUv; + private readonly IMemoryOwner transformInfoScratch; /// /// Stores the active base quantizer index for each frame superblock. /// - private readonly int[] quantizerIndices; + private readonly Buffer2D quantizerIndices; /// /// The base-2 number of constrained directional enhancement filter entries allocated per superblock. @@ -109,7 +175,7 @@ internal partial class Av1FrameInfo : IDisposable /// /// Stores constrained directional enhancement filter strengths grouped by superblock. /// - private readonly int[] cdefStrength; + private readonly Buffer2D cdefStrength; /// /// The base-2 number of loop-filter delta values stored per superblock. @@ -119,99 +185,259 @@ internal partial class Av1FrameInfo : IDisposable /// /// Stores the four loop-filter delta values for each superblock. /// - private readonly int[] deltaLoopFilter; + private readonly Buffer2D deltaLoopFilter; /// /// Stores raster-ordered loop-restoration units for each color plane. /// - private readonly Av1LoopRestorationUnit[][] loopRestorationUnits = [[], [], []]; + private InlineArray4?> loopRestorationUnits; /// /// Stores the number of loop-restoration unit columns for each color plane. /// - private readonly int[] loopRestorationUnitColumns = new int[Av1Constants.MaxPlanes]; + private InlineArray4 loopRestorationUnitColumns; /// /// The number of loop-restoration unit rows allocated for each color plane. /// - private readonly int[] loopRestorationUnitRows = new int[Av1Constants.MaxPlanes]; + private InlineArray4 loopRestorationUnitRows; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class using sequence-maximum dimensions. /// /// The sequence header defining maximum dimensions, superblock size, and color sampling. public Av1FrameInfo(ObuSequenceHeader sequenceHeader) + : this(Configuration.Default, sequenceHeader, sequenceHeader.MaxFrameWidth, sequenceHeader.MaxFrameHeight) + { + } + + /// + /// Initializes a new instance of the class for one active coded frame. + /// + /// The sequence header defining superblock size and color sampling. + /// The frame header defining the active coded dimensions. + public Av1FrameInfo(ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + : this( + Configuration.Default, + sequenceHeader, + frameHeader.FrameSize.FrameWidth, + frameHeader.FrameSize.FrameHeight) + { + } + + /// + /// Initializes a new instance of the class for one active coded frame. + /// + /// The decoder configuration providing frame-sized storage. + /// The sequence header defining superblock size and color sampling. + /// The frame header defining the active coded dimensions. + public Av1FrameInfo(Configuration configuration, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + : this( + configuration, + sequenceHeader, + frameHeader.FrameSize.FrameWidth, + frameHeader.FrameSize.FrameHeight) + { + } + + /// + /// Initializes a new instance of the class for explicit coded dimensions. + /// + /// The decoder configuration providing frame-sized storage. + /// The sequence header defining superblock size and color sampling. + /// The active coded frame width. + /// The active coded frame height. + private Av1FrameInfo(Configuration configuration, ObuSequenceHeader sequenceHeader, int frameWidth, int frameHeight) { - // Size frame-owned storage from the sequence maximums because later frame headers may select - // any coded dimensions up to these bounds without rebuilding the decoder's indexing model. + // A FrameInfo instance belongs to one coded frame, so transient syntax storage follows that frame rather + // than the potentially much larger sequence maximum declared by an untrusted stream. + this.memoryAllocator = configuration.MemoryAllocator; int superblockSizeLog2 = sequenceHeader.SuperblockSizeLog2; - int superblockAlignedWidth = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, superblockSizeLog2); - int superblockAlignedHeight = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameHeight, superblockSizeLog2); + int superblockAlignedWidth = Av1Math.AlignPowerOf2(frameWidth, superblockSizeLog2); + int superblockAlignedHeight = Av1Math.AlignPowerOf2(frameHeight, superblockSizeLog2); this.superblockColumnCount = superblockAlignedWidth >> superblockSizeLog2; this.superblockRowCount = superblockAlignedHeight >> superblockSizeLog2; int superblockCount = this.superblockColumnCount * this.superblockRowCount; this.modeInfoSizePerSuperblock = 1 << (superblockSizeLog2 - Av1Constants.ModeInfoSizeLog2); 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 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. this.subsamplingFactor = (subX && subY) ? 2 : (subX && !subY) ? 1 : (!subX && !subY) ? 0 : -1; Guard.IsFalse(this.subsamplingFactor == -1, nameof(this.subsamplingFactor), "Invalid combination of subsampling."); - int lumaCoefficientCountPerSuperblock = this.modeInfoCountPerSuperblock * CoefficientCountPerModeInfo; - int chromaCoefficientCountPerSuperblock = lumaCoefficientCountPerSuperblock >> this.subsamplingFactor; - this.coefficientsY = new int[superblockCount * lumaCoefficientCountPerSuperblock]; - this.coefficientsU = new int[superblockCount * chromaCoefficientCountPerSuperblock]; - this.coefficientsV = new int[superblockCount * chromaCoefficientCountPerSuperblock]; - this.quantizerIndices = new int[superblockCount]; - - // A 128x128 superblock contains four 64x64 CDEF filter blocks; a 64x64 superblock contains one. - this.cdefStrengthFactorLog2 = (superblockSizeLog2 - 6) << 1; - this.cdefStrength = new int[superblockCount << this.cdefStrengthFactorLog2]; - Array.Fill(this.cdefStrength, -1); - this.deltaLoopFilter = new int[superblockCount << this.deltaLoopFactorLog2]; + this.lumaCoefficientCount = this.modeInfoCountPerSuperblock * CoefficientCountPerModeInfo; + this.chromaCoefficientCount = sequenceHeader.ColorConfig.IsMonochrome + ? 0 + : this.lumaCoefficientCount >> this.subsamplingFactor; + + IMemoryOwner? allocatedCoefficientScratch = null; + MemoryGroup? allocatedModeInfos = null; + Buffer2D? allocatedModeInfoCounts = null; + Av1FrameModeInfoMap? allocatedModeInfoMap = null; + IMemoryOwner? allocatedTransformInfoScratch = null; + Buffer2D? allocatedQuantizerIndices = null; + Buffer2D? allocatedCdefStrength = null; + Buffer2D? 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(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(modeInfoLength, 1, clean); + allocatedModeInfoCounts = this.memoryAllocator.Allocate2D(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(transformInfoScratchLength, clean); + allocatedQuantizerIndices = this.memoryAllocator.Allocate2D(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(1 << this.cdefStrengthFactorLog2, superblockCount, clean); + allocatedCdefStrength.MemoryGroup.Fill(-1); + allocatedDeltaLoopFilter = this.memoryAllocator.Allocate2D(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; } /// /// Gets the total mode-information capacity allocated for the frame. /// - public int ModeInfoCount => this.modeInfos.Length; + public int ModeInfoCount => checked((int)this.modeInfos.TotalLength); /// /// Gets the width or height of one square superblock in 4x4 mode-information units. /// public int SuperblockModeInfoSize => this.modeInfoSizePerSuperblock; + /// + /// Gets a bit mask containing every luma transform type decoded in this frame. + /// + public int LumaTransformTypeCoverage { get; private set; } + + /// + /// Gets the inter-prediction features selected by coding blocks in this frame. + /// + public Av1InterPredictionFeatures InterPredictionFeatures { get; private set; } + + /// + /// Records one decoded luma transform type before the current-superblock scratch is reused. + /// + /// The decoded luma transform type. + public void RecordLumaTransformType(Av1TransformType transformType) => + this.LumaTransformTypeCoverage |= 1 << (int)transformType; + + /// + /// Records the inter-prediction features selected by one completed coding block. + /// + /// The completed block mode information. + /// The frame header containing global-motion parameters. + 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 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; + } + /// /// Initializes the active frame's contiguous segment map and applies whole-map inheritance when requested. /// /// The frame header defining active geometry and segmentation update behavior. - /// + /// /// The retained state selected by the primary reference, or when no primary reference exists. /// - public void InitializeSegmentIds(ObuFrameHeader frameHeader, Av1FrameInfo? primaryReferenceFrameInfo) + public void InitializeSegmentIds(ObuFrameHeader frameHeader, ReferenceState? primaryReferenceState) { ObuSegmentationParameters segmentationParameters = frameHeader.SegmentationParameters; if (!segmentationParameters.Enabled) @@ -223,16 +449,21 @@ internal partial class Av1FrameInfo : IDisposable this.segmentIdColumnCount = frameHeader.ModeInfoColumnCount; this.segmentIdRowCount = frameHeader.ModeInfoRowCount; - this.segmentIds = new byte[this.segmentIdColumnCount * this.segmentIdRowCount]; + this.segmentIds = this.memoryAllocator.Allocate2D( + this.segmentIdColumnCount, + this.segmentIdRowCount, + AllocationOptions.Clean); + Buffer2D? primarySegmentIds = primaryReferenceState?.SegmentIds; if (segmentationParameters.SegmentationUpdateMap == 0 && - primaryReferenceFrameInfo is not null && - primaryReferenceFrameInfo.segmentIdColumnCount == this.segmentIdColumnCount && - primaryReferenceFrameInfo.segmentIdRowCount == this.segmentIdRowCount) + primaryReferenceState is not null && + primarySegmentIds is not null && + 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 // 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 /// The zero-based mode-information row. /// The zero-based mode-information column. /// The segment identifier stored at the requested position. - public byte GetSegmentId(int row, int column) => this.segmentIds[(row * this.segmentIdColumnCount) + column]; + public byte GetSegmentId(int row, int column) + { + Buffer2D segmentIds = this.segmentIds + ?? this.referenceState?.SegmentIds + ?? throw new InvalidOperationException("The AV1 frame has no active segmentation map."); + + return segmentIds[column, row]; + } /// /// Gets the minimum retained segment identifier across a block's clipped mode-information coverage. /// - /// + /// /// The retained primary-frame state, or when no compatible map is available. /// /// The block size whose 4x4 coverage is inspected. @@ -255,12 +493,13 @@ internal partial class Av1FrameInfo : IDisposable /// /// The minimum retained segment identifier, or zero when no same-sized retained segmentation map is available. /// - public int GetPredictedSegmentId(Av1FrameInfo? primaryReferenceFrameInfo, Av1BlockSize blockSize, Point modeInfoPosition) + public int GetPredictedSegmentId(ReferenceState? primaryReferenceState, Av1BlockSize blockSize, Point modeInfoPosition) { - if (primaryReferenceFrameInfo is null || - primaryReferenceFrameInfo.segmentIds.Length == 0 || - primaryReferenceFrameInfo.segmentIdColumnCount != this.segmentIdColumnCount || - primaryReferenceFrameInfo.segmentIdRowCount != this.segmentIdRowCount) + Buffer2D? primarySegmentIds = primaryReferenceState?.SegmentIds; + if (primaryReferenceState is null || + primarySegmentIds is null || + 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 // 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. for (int row = 0; row < rowCount; row++) { - int offset = ((modeInfoPosition.Y + row) * primaryReferenceFrameInfo.segmentIdColumnCount) + modeInfoPosition.X; - ReadOnlySpan segmentRow = primaryReferenceFrameInfo.segmentIds.AsSpan(offset, columnCount); + ReadOnlySpan segmentRow = primarySegmentIds + .DangerousGetRowSpan(modeInfoPosition.Y + row) + .Slice(modeInfoPosition.X, columnCount); 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 rowCount = Math.Min(blockSize.Get4x4HighCount(), this.segmentIdRowCount - modeInfoPosition.Y); + Buffer2D 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 // row-major layout without the per-row object indirection of the previous jagged map. for (int row = 0; row < rowCount; row++) { - int offset = ((modeInfoPosition.Y + row) * this.segmentIdColumnCount) + modeInfoPosition.X; - this.segmentIds.AsSpan(offset, columnCount).Fill((byte)segmentId); + segmentIds + .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. int columnCount = Math.Max((planeWidth + (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.loopRestorationUnitRows[planeIndex] = rowCount; - this.loopRestorationUnits[planeIndex] = units; + this.loopRestorationUnits[planeIndex] = this.memoryAllocator.Allocate2D( + columnCount, + rowCount, + AllocationOptions.Clean); } } @@ -350,11 +591,17 @@ internal partial class Av1FrameInfo : IDisposable /// /// The position in the frame superblock grid. /// The superblock view. - public Av1SuperblockInfo GetSuperblock(Point index) + public Av1SuperblockInfo GetSuperblock(Point index) => new(this, index); + + /// + /// Gets the number of mode-information records parsed for a specified superblock. + /// + /// The position in the frame superblock grid. + /// The number of parsed records. + public int GetModeInfoCount(Point index) { - Span span = this.superblockInfos; - int i = (index.Y * this.superblockColumnCount) + index.X; - return span[i]; + int storageRow = (index.Y * this.superblockColumnCount) + index.X; + return this.modeInfoCounts[0, storageRow]; } /// @@ -373,8 +620,7 @@ internal partial class Av1FrameInfo : IDisposable public Av1BlockModeInfo GetModeInfo(Point superblockIndex, Point modeInfoIndex) { Point location = this.GetModeInfoPosition(superblockIndex, modeInfoIndex); - int index = this.modeInfoMap[location]; - return this.modeInfos[index]; + return this.GetModeInfoByStorageIndex(this.modeInfoMap[location]); } /// @@ -382,99 +628,74 @@ internal partial class Av1FrameInfo : IDisposable /// /// The frame-relative position in 4x4 mode-information units. /// The mode information covering the position. - public Av1BlockModeInfo GetModeInfoAt(Point modeInfoPosition) => this.modeInfos[this.modeInfoMap[modeInfoPosition]]; + public Av1BlockModeInfo GetModeInfoAt(Point modeInfoPosition) + => this.GetModeInfoByStorageIndex(this.modeInfoMap[modeInfoPosition]); /// /// Gets the mode information records parsed for the specified superblock in bitstream order. /// /// The position in the frame superblock grid. /// The number of parsed records to return. - /// The parsed mode-information records. - public Span GetModeInfos(Point superblockIndex, int count) + /// A discontiguous view of the parsed mode-information records. + public ModeInfoCollection GetModeInfos(Point superblockIndex, int count) { - Point location = this.GetModeInfoPosition(superblockIndex, Point.Empty); - int index = this.modeInfoMap[location]; - return this.modeInfos.AsSpan(index, count); + int storageRow = (superblockIndex.Y * this.superblockColumnCount) + superblockIndex.X; + return new ModeInfoCollection(this, storageRow * this.modeInfoCountPerSuperblock, count); } /// - /// Gets the transform-information storage for one plane of a specified superblock. + /// Gets the transform-information scratch for one plane of the current superblock. /// /// The zero-based plane index. - /// The position in the frame superblock grid. /// The luma storage for plane zero; otherwise, the shared chroma storage. - public Span GetSuperblockTransform(int plane, Point index) + public Span GetSuperblockTransform(int plane) { if (plane == 0) { - return this.GetSuperblockTransformY(index); + return this.GetSuperblockTransformY(); } - return this.GetSuperblockTransformUv(index); + return this.GetSuperblockTransformUv(); } /// - /// Gets the luma transform-information storage for a specified superblock. + /// Gets the luma transform-information scratch for the current superblock. /// - /// The position in the frame superblock grid. - /// The superblock luma transform-information span. - public Span GetSuperblockTransformY(Point index) - { - Span span = this.transformInfosY; - int offset = ((index.Y * this.superblockColumnCount) + index.X) * this.modeInfoCountPerSuperblock; - return span.Slice(offset, this.modeInfoCountPerSuperblock); - } + /// The current-superblock luma transform-information span. + public Span GetSuperblockTransformY() + => this.transformInfoScratch.GetSpan()[..this.modeInfoCountPerSuperblock]; /// - /// Gets the shared chroma transform-information storage for a specified superblock. + /// Gets the shared chroma transform-information scratch for the current superblock. /// - /// The position in the frame superblock grid. - /// The superblock chroma transform-information span. - public Span GetSuperblockTransformUv(Point index) - { - Span span = this.transformInfosUv; - int offset = (((index.Y * this.superblockColumnCount) + index.X) * this.modeInfoCountPerSuperblock) << 1; - return span.Slice(offset, this.modeInfoCountPerSuperblock << 1); - } + /// The current-superblock chroma transform-information span. + public Span GetSuperblockTransformUv() + => this.transformInfoScratch.GetSpan().Slice( + this.modeInfoCountPerSuperblock, + this.modeInfoCountPerSuperblock << 1); /// - /// Gets the luma coefficient storage for a specified superblock. + /// Gets the luma coefficient scratch reused for the current superblock. /// - /// The position in the frame superblock grid. - /// The superblock luma coefficient span. - public Span GetCoefficientsY(Point index) - { - Span span = this.coefficientsY; - int count = this.modeInfoCountPerSuperblock * CoefficientCountPerModeInfo; - int superblock = (index.Y * this.superblockColumnCount) + index.X; - return span.Slice(superblock * count, count); - } + /// The current superblock luma coefficient span. + public Span GetCoefficientsY() + => this.coefficientScratch.GetSpan()[..this.lumaCoefficientCount]; /// - /// Gets the blue-difference chroma coefficient storage for a specified superblock. + /// Gets the blue-difference chroma coefficient scratch reused for the current superblock. /// - /// The position in the frame superblock grid. - /// The superblock blue-difference chroma coefficient span. - public Span GetCoefficientsU(Point index) - { - Span span = this.coefficientsU; - int count = (this.modeInfoCountPerSuperblock * CoefficientCountPerModeInfo) >> this.subsamplingFactor; - int superblock = (index.Y * this.superblockColumnCount) + index.X; - return span.Slice(superblock * count, count); - } + /// The current superblock blue-difference chroma coefficient span. + public Span GetCoefficientsU() + => this.coefficientScratch.GetSpan().Slice(this.lumaCoefficientCount, this.chromaCoefficientCount); /// - /// Gets the red-difference chroma coefficient storage for a specified superblock. + /// Gets the red-difference chroma coefficient scratch reused for the current superblock. /// - /// The position in the frame superblock grid. - /// The superblock red-difference chroma coefficient span. - public Span GetCoefficientsV(Point index) - { - Span span = this.coefficientsV; - int count = (this.modeInfoCountPerSuperblock * CoefficientCountPerModeInfo) >> this.subsamplingFactor; - int superblock = (index.Y * this.superblockColumnCount) + index.X; - return span.Slice(superblock * count, count); - } + /// The current superblock red-difference chroma coefficient span. + public Span GetCoefficientsV() + => this.coefficientScratch.GetSpan().Slice( + this.lumaCoefficientCount + this.chromaCoefficientCount, + this.chromaCoefficientCount); /// /// Gets a reference to the active base quantizer index for a specified superblock. @@ -483,9 +704,8 @@ internal partial class Av1FrameInfo : IDisposable /// A reference to the superblock base quantizer index. public ref int GetQuantizerIndex(Point index) { - Span span = this.quantizerIndices; - int i = (index.Y * this.superblockColumnCount) + index.X; - return ref span[i]; + int storageRow = (index.Y * this.superblockColumnCount) + index.X; + return ref this.quantizerIndices[0, storageRow]; } /// @@ -495,9 +715,8 @@ internal partial class Av1FrameInfo : IDisposable /// The superblock filter-strength span. public Span GetCdefStrength(Point index) { - Span span = this.cdefStrength; - int i = ((index.Y * this.superblockColumnCount) + index.X) << this.cdefStrengthFactorLog2; - return span.Slice(i, 1 << this.cdefStrengthFactorLog2); + int storageRow = (index.Y * this.superblockColumnCount) + index.X; + return this.cdefStrength.DangerousGetRowSpan(storageRow); } /// @@ -520,9 +739,8 @@ internal partial class Av1FrameInfo : IDisposable /// The superblock loop-filter delta span. public Span GetDeltaLoopFilter(Point index) { - Span span = this.deltaLoopFilter; - int i = ((index.Y * this.superblockColumnCount) + index.X) << this.deltaLoopFactorLog2; - return span.Slice(i, 1 << this.deltaLoopFactorLog2); + int storageRow = (index.Y * this.superblockColumnCount) + index.X; + return this.deltaLoopFilter.DangerousGetRowSpan(storageRow); } /// @@ -546,10 +764,12 @@ internal partial class Av1FrameInfo : IDisposable /// The restoration-unit row. /// The restoration-unit column. /// The decoded restoration-unit information. - 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; - return this.loopRestorationUnits[plane][index]; + Buffer2D units = this.loopRestorationUnits[plane] + ?? throw new InvalidOperationException("The selected AV1 plane has no loop-restoration units."); + + return ref units[column, row]; } /// @@ -560,10 +780,27 @@ internal partial class Av1FrameInfo : IDisposable public void UpdateModeInfo(Av1BlockModeInfo modeInfo, Av1SuperblockInfo superblockInfo) { 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; - this.modeInfos[this.modeInfoMap.NextIndex] = modeInfo; + this.GetModeInfoByStorageIndex(storageIndex) = modeInfo; + modeInfoCount++; this.UpdateRetainedMotionField(modeInfo, modeInfoPosition); - this.modeInfoMap.Update(modeInfoPosition, modeInfo.BlockSize); + this.modeInfoMap.Update(modeInfoPosition, modeInfo.BlockSize, storageIndex); + } + + /// + /// Gets a reference to one packed mode-information record across allocator segments. + /// + /// The packed frame-storage index. + /// A reference to the selected mode information. + 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]; } /// @@ -578,4 +815,85 @@ internal partial class Av1FrameInfo : IDisposable int y = (superblockPosition.Y * this.modeInfoSizePerSuperblock) + positionInSuperblock.Y; return new Point(x, y); } + + /// + /// Provides indexed and reference-preserving traversal over one superblock's discontiguous mode information. + /// + public readonly struct ModeInfoCollection + { + private readonly Av1FrameInfo owner; + private readonly int startIndex; + + /// + /// Initializes a new instance of the struct. + /// + /// The frame that owns the mode-information group. + /// The packed index of the first record. + /// The number of records in the view. + public ModeInfoCollection(Av1FrameInfo owner, int startIndex, int length) + { + this.owner = owner; + this.startIndex = startIndex; + this.Length = length; + } + + /// + /// Gets the number of records in the view. + /// + public int Length { get; } + + /// + /// Gets a reference to the record at the specified traversal index. + /// + /// The zero-based traversal index. + public ref Av1BlockModeInfo this[int index] => + ref this.owner.GetModeInfoByStorageIndex(this.startIndex + index); + + /// + /// Creates a reference-preserving enumerator over the records. + /// + /// The initialized enumerator. + public Enumerator GetEnumerator() => new(this.owner, this.startIndex, this.Length); + + /// + /// Enumerates one superblock's mode-information records without flattening allocator segments. + /// + public struct Enumerator + { + private readonly Av1FrameInfo owner; + private readonly int startIndex; + private readonly int length; + private int index; + + /// + /// Initializes a new instance of the struct. + /// + /// The frame that owns the mode-information group. + /// The packed index of the first record. + /// The number of records in the view. + public Enumerator(Av1FrameInfo owner, int startIndex, int length) + { + this.owner = owner; + this.startIndex = startIndex; + this.length = length; + this.index = -1; + } + + /// + /// Gets a reference to the current record. + /// + public ref Av1BlockModeInfo Current => + ref this.owner.GetModeInfoByStorageIndex(this.startIndex + this.index); + + /// + /// Advances to the next record. + /// + /// when another record is available. + public bool MoveNext() + { + this.index++; + return this.index < this.length; + } + } + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameModeInfoMap.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameModeInfoMap.cs index 5ac36b199..31e1321a4 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameModeInfoMap.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameModeInfoMap.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Memory; + namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; /// @@ -11,12 +13,12 @@ internal partial class Av1FrameInfo /// /// Mapping of values, from position to index into the . /// - public class Av1FrameModeInfoMap + public sealed class Av1FrameModeInfoMap : IDisposable { /// /// Stores the mode-information index assigned to each aligned 4x4 frame location. /// - private readonly int[] offsets; + private readonly MemoryGroup offsets; /// /// The dimensions of in 4x4 mode-information units. @@ -24,14 +26,25 @@ internal partial class Av1FrameInfo private readonly Size alignedModeInfoCount; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class using the default allocator. /// /// The aligned frame dimensions in 4x4 mode-information units. public Av1FrameModeInfoMap(Size modeInfoCount) + : this(Configuration.Default.MemoryAllocator, modeInfoCount) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The allocator providing frame-sized storage. + /// The aligned frame dimensions in 4x4 mode-information units. + public Av1FrameModeInfoMap(MemoryAllocator memoryAllocator, Size modeInfoCount) { this.alignedModeInfoCount = modeInfoCount; 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(offsetCount, 1, AllocationOptions.Clean); } /// @@ -47,8 +60,8 @@ internal partial class Av1FrameInfo { get { - int index = (location.Y * this.alignedModeInfoCount.Width) + location.X; - return this.offsets[index]; + long offset = ((long)location.Y * this.alignedModeInfoCount.Width) + location.X; + return this.offsets.GetRemainingSliceOfBuffer(offset)[0]; } } @@ -57,7 +70,8 @@ internal partial class Av1FrameInfo /// /// The block origin in 4x4 mode-information units. /// The decoded block size. - public void Update(Point modeInfoLocation, Av1BlockSize blockSize) + /// The packed frame-storage index assigned to the decoded block. + public void Update(Point modeInfoLocation, Av1BlockSize blockSize, int storageIndex) { int bw4 = blockSize.Get4x4WideCount(); int bh4 = blockSize.Get4x4HighCount(); @@ -70,10 +84,30 @@ internal partial class Av1FrameInfo // because later blocks query their above and left neighbors at cell granularity. 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 destination = this.offsets.GetRemainingSliceOfBuffer(offset); + int count = Math.Min(remaining, destination.Length); + destination[..count].Fill(storageIndex); + offset += count; + remaining -= count; + } } this.NextIndex++; } + + /// + /// Maps every 4x4 location covered by a decoded block to its traversal-order index. + /// + /// The block origin in 4x4 mode-information units. + /// The decoded block size. + public void Update(Point modeInfoLocation, Av1BlockSize blockSize) + => this.Update(modeInfoLocation, blockSize, this.NextIndex); + + /// + public void Dispose() => this.offsets.Dispose(); } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LoopRestorationUnit.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LoopRestorationUnit.cs index d6d186912..c0765184f 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LoopRestorationUnit.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LoopRestorationUnit.cs @@ -1,37 +1,34 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; /// /// Stores the filter selection and coefficients decoded for one AV1 loop-restoration unit. /// -internal class Av1LoopRestorationUnit +internal struct Av1LoopRestorationUnit { /// - /// Initializes a new instance of the class. + /// The three transmitted symmetric vertical Wiener coefficients. /// - public Av1LoopRestorationUnit() - { - this.WienerVertical = new int[Av1Constants.WienerCoefficientCount]; - this.WienerHorizontal = new int[Av1Constants.WienerCoefficientCount]; - this.SgrProjectionCoefficients = new int[2]; - } + public WienerCoefficientBuffer WienerVertical; /// - /// Gets or sets the restoration filter selected for the unit. + /// The three transmitted symmetric horizontal Wiener coefficients. /// - public Av1RestorationFilterType FilterType { get; set; } + public WienerCoefficientBuffer WienerHorizontal; /// - /// Gets the three transmitted symmetric vertical Wiener coefficients. + /// The two self-guided projection coefficients. /// - public int[] WienerVertical { get; } + public SgrProjectionCoefficientBuffer SgrProjectionCoefficients; /// - /// Gets the three transmitted symmetric horizontal Wiener coefficients. + /// Gets or sets the restoration filter selected for the unit. /// - public int[] WienerHorizontal { get; } + public Av1RestorationFilterType FilterType { get; set; } /// /// Gets or sets the self-guided filter parameter-set index. @@ -39,7 +36,20 @@ internal class Av1LoopRestorationUnit public int SgrParameterSet { get; set; } /// - /// Gets the two self-guided projection coefficients. + /// Stores the transmitted coefficients inline with the restoration unit. /// - public int[] SgrProjectionCoefficients { get; } + [InlineArray(Av1Constants.WienerCoefficientCount)] + public struct WienerCoefficientBuffer + { + private int element0; + } + + /// + /// Stores the projection coefficients inline with the restoration unit. + /// + [InlineArray(2)] + public struct SgrProjectionCoefficientBuffer + { + private int element0; + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs index 70e6187fa..38ddf2cd1 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs @@ -286,14 +286,4 @@ internal sealed class Av1NeighborArrayUnit : IDisposable } } } - - /// - /// Writes a DC-sign context across selected block edges. - /// - /// The encoded DC-sign context. - /// The block origin in samples. - /// The block dimensions in samples. - /// The neighbor arrays to update. - /// The byte-specific write path is not implemented. - public void UnitModeWrite(Span dcSignSpan, Point blockOrigin, Size blockSize, Av1NeighborArrayUnit.UnitMask unitMask) => throw new NotImplementedException(); } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionContext.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionContext.cs index b00b6a563..31af3be2d 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionContext.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionContext.cs @@ -43,14 +43,14 @@ internal struct Av1PartitionContext : IMinMaxValue } /// - /// Gets the maximum representable partition context. + /// Gets the sentinel partition context used for an unpopulated neighbor. /// - public static Av1PartitionContext MaxValue => throw new NotImplementedException(); + public static Av1PartitionContext MaxValue => new(byte.MaxValue, byte.MaxValue); /// - /// Gets the minimum representable partition context. + /// Gets the partition context with no split levels recorded. /// - public static Av1PartitionContext MinValue => throw new NotImplementedException(); + public static Av1PartitionContext MinValue => default; /// /// Gets or sets the five-bit context derived from the left neighbor. diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1SuperblockInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1SuperblockInfo.cs index 8ec50cf35..638cef860 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1SuperblockInfo.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1SuperblockInfo.cs @@ -6,15 +6,15 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; /// /// Stores the partition tree and decoded mode information for one AV1 superblock. /// -internal class Av1SuperblockInfo +internal readonly struct Av1SuperblockInfo { /// - /// Provides the frame-owned arrays addressed by this superblock view. + /// Provides the frame-owned state and current-superblock scratch used by this view. /// private readonly Av1FrameInfo frameInfo; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// /// The owning frame information. /// The superblock position in the frame superblock grid. @@ -45,19 +45,19 @@ internal class Av1SuperblockInfo public Av1BlockModeInfo SuperblockModeInfo => this.GetModeInfo(new Point(0, 0)); /// - /// Gets the luma coefficient storage reserved for this superblock. + /// Gets the luma coefficient scratch for the current superblock. /// - public Span CoefficientsY => this.frameInfo.GetCoefficientsY(this.Position); + public Span CoefficientsY => this.frameInfo.GetCoefficientsY(); /// - /// Gets the blue-difference chroma coefficient storage reserved for this superblock. + /// Gets the blue-difference chroma coefficient scratch for the current superblock. /// - public Span CoefficientsU => this.frameInfo.GetCoefficientsU(this.Position); + public Span CoefficientsU => this.frameInfo.GetCoefficientsU(); /// - /// Gets the red-difference chroma coefficient storage reserved for this superblock. + /// Gets the red-difference chroma coefficient scratch for the current superblock. /// - public Span CoefficientsV => this.frameInfo.GetCoefficientsV(this.Position); + public Span CoefficientsV => this.frameInfo.GetCoefficientsV(); /// /// Gets the constrained directional enhancement filter strengths for this superblock. @@ -70,44 +70,35 @@ internal class Av1SuperblockInfo public Span SuperblockDeltaLoopFilter => this.frameInfo.GetDeltaLoopFilter(this.Position); /// - /// Gets or sets the next luma transform-information index while parsing this superblock. + /// Gets the number of mode-information records parsed for this superblock. /// - public int TransformInfoIndexY { get; set; } + public int BlockCount => this.frameInfo.GetModeInfoCount(this.Position); /// - /// Gets or sets the next shared chroma transform-information index while parsing this superblock. + /// Gets the luma transform-information scratch for the current superblock. /// - public int TransformInfoIndexUv { get; set; } + /// The current-superblock luma transform-information span. + public Span GetTransformInfoY() => this.frameInfo.GetSuperblockTransformY(); /// - /// Gets or sets the number of mode-information records parsed for this superblock. + /// Gets the shared chroma transform-information scratch for the current superblock. /// - public int BlockCount { get; set; } - - /// - /// Gets the luma transform-information storage reserved for this superblock. - /// - /// The superblock luma transform-information span. - public Span GetTransformInfoY() => this.frameInfo.GetSuperblockTransformY(this.Position); - - /// - /// Gets the shared chroma transform-information storage reserved for this superblock. - /// - /// The superblock chroma transform-information span. - public Span GetTransformInfoUv() => this.frameInfo.GetSuperblockTransformUv(this.Position); + /// The current-superblock chroma transform-information span. + public Span GetTransformInfoUv() => this.frameInfo.GetSuperblockTransformUv(); /// /// Gets the transform-information storage for the specified color plane. /// /// The zero-based color-plane index. /// The transform-information span for the plane. - public Span GetTransformInfo(int plane) => this.frameInfo.GetSuperblockTransform(plane, this.Position); + public Span GetTransformInfo(int plane) => this.frameInfo.GetSuperblockTransform(plane); /// /// Gets the mode information records parsed for this superblock in bitstream order. /// /// The mode information records for the superblock. - public Span GetModeInfos() => this.frameInfo.GetModeInfos(this.Position, this.BlockCount); + public Av1FrameInfo.ModeInfoCollection GetModeInfos() => + this.frameInfo.GetModeInfos(this.Position, this.BlockCount); /// /// Gets the mode information covering a position relative to this superblock. diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileInfo.cs index b5cb18d5b..26e0e9526 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileInfo.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileInfo.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; /// /// Describes one AV1 tile's superblock and mode-information boundaries. /// -internal class Av1TileInfo +internal sealed class Av1TileInfo { /// /// Initializes a new instance of the class for the specified tile coordinates. diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs index 31daba54d..4df5cfb8f 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs @@ -79,22 +79,22 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable /// /// Stores the loop-filter delta values carried between superblocks in the current tile. /// - private readonly int[] currentDeltaLoopFilter = new int[Av1Constants.FrameLoopFilterCount]; + private InlineArray4 currentDeltaLoopFilter; /// /// Stores per-plane transform counts for each forced 64x64 residual region. /// - private readonly int[][] transformUnitCount; + private InlineArray4> transformUnitCount; /// /// Tracks the first unassigned transform-information index for luma and shared chroma storage. /// - private readonly int[] firstTransformOffset = new int[2]; + private InlineArray4 firstTransformOffset; /// /// Tracks the next coefficient slot for each color plane within the current superblock. /// - private readonly int[] coefficientIndex = []; + private InlineArray4 coefficientIndex; /// /// Reusable padded coefficient-context storage for the sequential transform traversal. @@ -154,7 +154,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable /// /// The retained primary frame whose segment map supplies temporal segment-ID predictions. /// - private readonly Av1FrameInfo? primaryReferenceFrameInfo; + private readonly Av1FrameInfo.ReferenceState? primaryReferenceState; /// /// The retained reconstructed frames used to determine reference scaling during inter mode parsing. @@ -192,7 +192,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable Av1FrameEntropyContexts entropyContexts, Av1FrameEntropyContext? primaryReferenceContext, Av1ReferenceFrameStore? referenceFrames) - : this(configuration, sequenceHeader, frameHeader, entropyContexts, primaryReferenceContext, referenceFrames, null, null, true) + : this(configuration, sequenceHeader, frameHeader, entropyContexts, primaryReferenceContext, referenceFrames, null) { } @@ -225,9 +225,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable entropyContexts, primaryReferenceContext, referenceFrames, - lumaPaletteColorIndexMap, - chromaPaletteColorIndexMap, - false) + new PaletteColorIndexMaps(lumaPaletteColorIndexMap, chromaPaletteColorIndexMap)) { } @@ -240,9 +238,9 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable /// The entropy contexts reused by the owning decoder session. /// The retained primary-reference entropy context. /// The retained reconstructed frames. - /// A shared luma palette map, or when the reader owns one. - /// A shared chroma palette map, or when the reader owns one. - /// Whether the reader owns and disposes the palette maps. + /// + /// Shared palette maps, or when the reader allocates and owns both maps. + /// private Av1TileReader( Configuration configuration, ObuSequenceHeader sequenceHeader, @@ -250,45 +248,47 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable Av1FrameEntropyContexts entropyContexts, Av1FrameEntropyContext? primaryReferenceContext, Av1ReferenceFrameStore? referenceFrames, - Buffer2D? lumaPaletteColorIndexMap, - Buffer2D? chromaPaletteColorIndexMap, - bool ownsPaletteColorIndexMaps) + PaletteColorIndexMaps? sharedPaletteColorIndexMaps) { this.FrameHeader = frameHeader; this.configuration = configuration; this.SequenceHeader = sequenceHeader; this.entropyContexts = entropyContexts; this.referenceFrames = referenceFrames; - this.ownsPaletteColorIndexMaps = ownsPaletteColorIndexMaps; + this.ownsPaletteColorIndexMaps = sharedPaletteColorIndexMaps is null; this.entropyContexts.BeginFrame(frameHeader.QuantizationParameters.BaseQIndex, primaryReferenceContext); - // FrameInfo owns all traversal-order records and coefficient storage produced by the tile readers. - this.FrameInfo = new(this.SequenceHeader); + // FrameInfo owns traversal records for this coded frame and one superblock of coefficient scratch. + this.FrameInfo = new(this.configuration, this.SequenceHeader, this.FrameHeader); if (referenceFrames is not null) { byte? primaryReferenceSlot = this.FrameHeader.PrimaryReferenceSlot; if (primaryReferenceSlot is not null) { - // The uncompressed-header parser has already validated this slot. Keep only its frame-state owner; - // segment samples remain in the retained frame and are copied only for whole-map inheritance. - this.primaryReferenceFrameInfo = referenceFrames.Resolve(primaryReferenceSlot.Value)!.FrameInfo; + // The uncompressed-header parser has already validated this slot. Segment samples remain in the + // retained reference state and are copied only for whole-map inheritance. + this.primaryReferenceState = referenceFrames.ResolveRequired(primaryReferenceSlot.Value).ReferenceState; } } - this.FrameInfo.InitializeSegmentIds(this.FrameHeader, this.primaryReferenceFrameInfo); - this.FrameInfo.InitializeLoopRestoration(this.SequenceHeader, this.FrameHeader); + try + { + this.FrameInfo.InitializeSegmentIds(this.FrameHeader, this.primaryReferenceState); + this.FrameInfo.InitializeLoopRestoration(this.SequenceHeader, this.FrameHeader); + } + catch + { + // FrameInfo has already rented the active frame's syntax storage. Return every successful rent if + // a later segmentation or restoration allocation prevents this reader from being constructed. + this.FrameInfo.Dispose(); + throw; + } // Above contexts span the aligned frame width, while left contexts are reused for each superblock row. int planesCount = sequenceHeader.ColorConfig.PlaneCount; - int superblockColumnCount = - Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, sequenceHeader.SuperblockSizeLog2) >> sequenceHeader.SuperblockSizeLog2; - int modeInfoWideColumnCount = superblockColumnCount * sequenceHeader.SuperblockModeInfoSize; - modeInfoWideColumnCount = Av1Math.AlignPowerOf2(modeInfoWideColumnCount, sequenceHeader.SuperblockSizeLog2 - Av1Constants.ModeInfoSizeLog2); - this.transformUnitCount = new int[Av1Constants.MaxPlanes][]; - this.transformUnitCount[0] = new int[this.FrameInfo.ModeInfoCount]; - this.transformUnitCount[1] = new int[this.FrameInfo.ModeInfoCount]; - this.transformUnitCount[2] = new int[this.FrameInfo.ModeInfoCount]; - this.coefficientIndex = new int[Av1Constants.MaxPlanes]; + int modeInfoWideColumnCount = Av1Math.AlignPowerOf2( + frameHeader.ModeInfoColumnCount, + sequenceHeader.SuperblockSizeLog2 - Av1Constants.ModeInfoSizeLog2); this.aboveNeighborContext = new Av1ParseAboveNeighbor4x4Context(configuration, planesCount, modeInfoWideColumnCount); try @@ -317,7 +317,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable throw; } - if (ownsPaletteColorIndexMaps) + if (sharedPaletteColorIndexMaps is null) { Buffer2D? ownedLumaPaletteColorIndexMap = null; Buffer2D? ownedChromaPaletteColorIndexMap = null; @@ -345,8 +345,9 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable } else { - this.lumaPaletteColorIndexMap = lumaPaletteColorIndexMap!; - this.chromaPaletteColorIndexMap = chromaPaletteColorIndexMap!; + PaletteColorIndexMaps paletteColorIndexMaps = sharedPaletteColorIndexMaps.Value; + this.lumaPaletteColorIndexMap = paletteColorIndexMaps.Luma; + this.chromaPaletteColorIndexMap = paletteColorIndexMaps.Chroma; } if (referenceFrames is not null) @@ -457,6 +458,11 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable /// public Av1FrameEntropyContext FrameEntropyContext => this.entropyContexts.Published; + /// + /// Gets the decoder-session entropy owner that publishes the completed frame context. + /// + public Av1FrameEntropyContexts EntropyContexts => this.entropyContexts; + /// /// Returns tile-neighbor storage and the reader's frame-state lease to the configured memory allocator. /// @@ -536,10 +542,13 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable Av1SuperblockInfo superblockInfo = this.FrameInfo.GetSuperblock(superblockPosition); Point modeInfoPosition = new(column, row); + superblockInfo.CoefficientsY.Clear(); + superblockInfo.CoefficientsU.Clear(); + superblockInfo.CoefficientsV.Clear(); this.FrameInfo.ClearCdef(superblockPosition); this.firstTransformOffset[0] = 0; this.firstTransformOffset[1] = 0; - this.coefficientIndex.AsSpan().Clear(); + this.coefficientIndex[..Av1Constants.MaxPlanes].Clear(); this.ReadLoopRestoration(ref reader, modeInfoPosition, superBlockSize); this.ParsePartition(ref reader, modeInfoPosition, superBlockSize, superblockInfo, tileInfo); @@ -576,7 +585,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable /// Resets the loop-filter delta predictors before parsing a tile. /// private void ClearLoopFilterDelta() - => this.currentDeltaLoopFilter.AsSpan().Clear(); + => this.currentDeltaLoopFilter[..Av1Constants.FrameLoopFilterCount].Clear(); /// /// Reads loop-restoration unit syntax that begins at a superblock location. @@ -629,8 +638,8 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable { for (int unitColumn = unitColumnStart; unitColumn < unitColumnEnd; unitColumn++) { - Av1LoopRestorationUnit unit = this.FrameInfo.GetLoopRestorationUnit(plane, unitRow, unitColumn); - this.ReadLoopRestorationUnit(ref reader, item.Type, plane, unit); + ref Av1LoopRestorationUnit unit = ref this.FrameInfo.GetLoopRestorationUnit(plane, unitRow, unitColumn); + this.ReadLoopRestorationUnit(ref reader, item.Type, plane, ref unit); } } } @@ -647,7 +656,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable ref Av1SymbolDecoder reader, ObuRestorationType frameType, int plane, - Av1LoopRestorationUnit unit) + ref Av1LoopRestorationUnit unit) { unit.FilterType = frameType switch { @@ -663,11 +672,11 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable if (unit.FilterType == Av1RestorationFilterType.Wiener) { - this.ReadWienerFilter(ref reader, plane, unit); + this.ReadWienerFilter(ref reader, plane, ref unit); } else if (unit.FilterType == Av1RestorationFilterType.SgrProjection) { - this.ReadSgrProjectionFilter(ref reader, plane, unit); + this.ReadSgrProjectionFilter(ref reader, plane, ref unit); } } @@ -677,13 +686,20 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable /// The tile symbol decoder. /// The zero-based color-plane index. /// The destination restoration-unit information. - private void ReadWienerFilter(ref Av1SymbolDecoder reader, int plane, Av1LoopRestorationUnit unit) + private void ReadWienerFilter(ref Av1SymbolDecoder reader, int plane, ref Av1LoopRestorationUnit unit) { for (int pass = 0; pass < 2; pass++) { - int[] destination = pass == 0 ? unit.WienerVertical : unit.WienerHorizontal; int firstCoefficient = plane == 0 ? 0 : 1; - destination[0] = 0; + if (pass == 0) + { + unit.WienerVertical[0] = 0; + } + else + { + unit.WienerHorizontal[0] = 0; + } + for (int coefficient = firstCoefficient; coefficient < Av1Constants.WienerCoefficientCount; coefficient++) { int referenceIndex = (((plane * 2) + pass) * Av1Constants.WienerCoefficientCount) + coefficient; @@ -694,7 +710,15 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable this.referenceLrWiener[referenceIndex] - minimum); value += minimum; - destination[coefficient] = value; + if (pass == 0) + { + unit.WienerVertical[coefficient] = value; + } + else + { + unit.WienerHorizontal[coefficient] = value; + } + this.referenceLrWiener[referenceIndex] = value; } } @@ -706,11 +730,11 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable /// The tile symbol decoder. /// The zero-based color-plane index. /// The destination restoration-unit information. - private void ReadSgrProjectionFilter(ref Av1SymbolDecoder reader, int plane, Av1LoopRestorationUnit unit) + private void ReadSgrProjectionFilter(ref Av1SymbolDecoder reader, int plane, ref Av1LoopRestorationUnit unit) { unit.SgrParameterSet = reader.ReadLiteral(4); int[] radii = SgrProjectionRadii[unit.SgrParameterSet]; - int[] coefficients = unit.SgrProjectionCoefficients; + Span coefficients = unit.SgrProjectionCoefficients; Span allReferences = this.referenceSgrXqd; Span references = allReferences.Slice(plane * 2, 2); if (radii[0] == 0) @@ -940,7 +964,6 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable Av1PartitionInfo partitionInfo = new(blockModeInfo, superblockInfo, hasChroma, partitionType); partitionInfo.ColumnIndex = columnIndex; partitionInfo.RowIndex = rowIndex; - superblockInfo.BlockCount++; partitionInfo.ComputeBoundaryOffsets(this.SequenceHeader, this.FrameHeader, tileInfo); if (hasChroma) { @@ -969,6 +992,9 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable this.Residual(ref reader, ref partitionInfo, superblockInfo, tileInfo, blockSize); + // Record compact frame evidence before later frames release this frame's full mode-information graph. + this.FrameInfo.RecordInterPredictionFeatures(partitionInfo.ModeInfo, this.FrameHeader); + // Store the record only after all syntax has populated it, then map every covered 4x4 position. this.FrameInfo.UpdateModeInfo(partitionInfo.ModeInfo, superblockInfo); } @@ -1030,8 +1056,8 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable ? (maxBlocksWide * maxBlocksHigh) >> subSampling : partitionInfo.ModeInfo.GetTransformUnitCount(Av1PlaneType.Uv); - int lumaTransformInfoIndex = superblockInfo.TransformInfoIndexY + partitionInfo.ModeInfo.GetFirstTransformLocation(Av1PlaneType.Y); - int chromaBlueTransformInfoIndex = superblockInfo.TransformInfoIndexUv + partitionInfo.ModeInfo.GetFirstTransformLocation(Av1PlaneType.Uv); + int lumaTransformInfoIndex = partitionInfo.ModeInfo.GetFirstTransformLocation(Av1PlaneType.Y); + int chromaBlueTransformInfoIndex = partitionInfo.ModeInfo.GetFirstTransformLocation(Av1PlaneType.Uv); int chromaRedTransformInfoIndex = chromaBlueTransformInfoIndex + chromaTransformUnitCount; int forceSplitCount = 0; @@ -1256,6 +1282,11 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable ref transformInfo, coefficientBuffer); + if (plane == 0) + { + this.FrameInfo.RecordLumaTransformType(transformInfo.Type); + } + return endOfBlock; } @@ -1348,7 +1379,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable int aboveOffset, int leftOffset) { - Av1TransformBlockContext transformBlockContext = new(); + Av1TransformBlockContext transformBlockContext = default; ReadOnlySpan aboveContext = this.aboveNeighborContext.GetContext(plane)[aboveOffset..]; ReadOnlySpan leftContext = this.leftNeighborContext.GetContext(plane)[leftOffset..]; int dcSign = 0; @@ -1576,9 +1607,9 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable ref Av1BlockModeInfo modeInfo = ref partitionInfo.ModeInfo; bool usesInterTransformSyntax = modeInfo.ReferenceFrames[0] >= Av1ReferenceFrameType.Last || modeInfo.UseIntraBlockCopy; - this.transformUnitCount[(int)Av1Plane.Y].AsSpan(0, 4).Clear(); - this.transformUnitCount[(int)Av1Plane.U].AsSpan(0, 4).Clear(); - this.transformUnitCount[(int)Av1Plane.V].AsSpan(0, 4).Clear(); + this.transformUnitCount[(int)Av1Plane.Y][..4].Clear(); + this.transformUnitCount[(int)Av1Plane.U][..4].Clear(); + this.transformUnitCount[(int)Av1Plane.V][..4].Clear(); if (usesInterTransformSyntax && !modeInfo.Skip && @@ -1921,7 +1952,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable if (this.FrameDecoder is not null) { - modeInfo.SetPaletteColorIndexMap(Av1PlaneType.Y, colorIndexMap); + modeInfo.SetPaletteColorIndexMap(Av1PlaneType.Y, colorIndexMap.Bounds); } } @@ -1954,7 +1985,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable if (this.FrameDecoder is not null) { - modeInfo.SetPaletteColorIndexMap(Av1PlaneType.Uv, colorIndexMap); + modeInfo.SetPaletteColorIndexMap(Av1PlaneType.Uv, colorIndexMap.Bounds); } } } @@ -2239,7 +2270,10 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable { int canonicalReferenceIndex = (int)referenceFrame - (int)Av1ReferenceFrameType.Last; uint referenceSlot = this.FrameHeader.GetReferenceFrameIndices()[canonicalReferenceIndex]; - Av1FrameBuffer referenceFrameBuffer = this.referenceFrames!.Resolve((int)referenceSlot)!.FrameBuffer; + Av1ReferenceFrameStore referenceFrames = this.referenceFrames + ?? throw new InvalidImageContentException("AV1 warped-motion syntax requires a reconstructed reference map."); + + Av1FrameBuffer referenceFrameBuffer = referenceFrames.ResolveRequired((int)referenceSlot).FrameBuffer; // Local warped motion is excluded for a scaled reference. Width and height equality are the // identity-scale test because both dimensions form the decoder's reference scale factors. @@ -3127,7 +3161,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable { // The frame map was inherited as one contiguous copy during reader construction. Resolve the same clipped // minimum that the reference decoder obtains from last_frame_seg_map so block state and the already copied map agree. - modeInfo.SegmentId = this.FrameInfo.GetPredictedSegmentId(this.primaryReferenceFrameInfo, modeInfo.BlockSize, modeInfoPosition); + modeInfo.SegmentId = this.FrameInfo.GetPredictedSegmentId(this.primaryReferenceState, modeInfo.BlockSize, modeInfoPosition); return; } @@ -3160,7 +3194,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable modeInfo.SegmentIdPredicted = reader.ReadSegmentIdPredicted(context); if (modeInfo.SegmentIdPredicted) { - modeInfo.SegmentId = this.FrameInfo.GetPredictedSegmentId(this.primaryReferenceFrameInfo, modeInfo.BlockSize, modeInfoPosition); + modeInfo.SegmentId = this.FrameInfo.GetPredictedSegmentId(this.primaryReferenceState, modeInfo.BlockSize, modeInfoPosition); } else { @@ -3319,7 +3353,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable // Delta-LF values are predicted across superblocks within a tile, but every block in one superblock observes // the same resulting values. Snapshot the predictors so later filtering does not depend on parse order. - this.currentDeltaLoopFilter.AsSpan().CopyTo(partitionInfo.SuperblockInfo.SuperblockDeltaLoopFilter); + this.currentDeltaLoopFilter[..Av1Constants.FrameLoopFilterCount].CopyTo(partitionInfo.SuperblockInfo.SuperblockDeltaLoopFilter); } /// @@ -3387,7 +3421,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable { // Reference feature values use the same numeric labels as Av1ReferenceFrameType. INTRA_FRAME is zero; // every canonical inter reference begins at LAST_FRAME and therefore has a positive value. - int referenceFrame = segmentationParameters.FeatureData[segmentId, (int)ObuSegmentationLevelFeature.ReferenceFrame]; + int referenceFrame = segmentationParameters.GetFeatureData(segmentId, (int)ObuSegmentationLevelFeature.ReferenceFrame); return referenceFrame >= (int)Av1ReferenceFrameType.Last; } @@ -3421,9 +3455,9 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable int segmentId = modeInfo.SegmentId; if (segmentationParameters.IsFeatureActive(segmentId, ObuSegmentationLevelFeature.ReferenceFrame)) { - references[0] = (Av1ReferenceFrameType)segmentationParameters.FeatureData[ + references[0] = (Av1ReferenceFrameType)segmentationParameters.GetFeatureData( segmentId, - (int)ObuSegmentationLevelFeature.ReferenceFrame]; + (int)ObuSegmentationLevelFeature.ReferenceFrame); references[1] = Av1ReferenceFrameType.None; return; @@ -3677,6 +3711,22 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable } } + /// + /// Carries the two shared palette maps as one valid constructor state. + /// + internal readonly struct PaletteColorIndexMaps(Buffer2D luma, Buffer2D chroma) + { + /// + /// Gets the shared luma palette map. + /// + public Buffer2D Luma { get; } = luma; + + /// + /// Gets the shared chroma palette map. + /// + public Buffer2D Chroma { get; } = chroma; + } + /// /// Provides inline storage for the two self-guided restoration coefficients of each of the three AV1 planes. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs index c6e845b4d..493f4d7a6 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs @@ -764,7 +764,7 @@ internal partial class Av1TileWriter dcSignSpan, blockOrigin, size, - Av1NeighborArrayUnit.UnitMask.Left | Av1NeighborArrayUnit.UnitMask.Top); + Av1NeighborArrayUnit.UnitMask.Left | Av1NeighborArrayUnit.UnitMask.Top); if (blk_geom.HasUv) { @@ -772,12 +772,12 @@ internal partial class Av1TileWriter dcSignSpan, ((blockOrigin >> 3) << 3) >> 1, size, - Av1NeighborArrayUnit.UnitMask.Left | Av1NeighborArrayUnit.UnitMask.Top); + Av1NeighborArrayUnit.UnitMask.Left | Av1NeighborArrayUnit.UnitMask.Top); cr_dc_sign_level_coeff_na.UnitModeWrite( dcSignSpan, ((blockOrigin >> 3) << 3) >> 1, size, - Av1NeighborArrayUnit.UnitMask.Left | Av1NeighborArrayUnit.UnitMask.Top); + Av1NeighborArrayUnit.UnitMask.Left | Av1NeighborArrayUnit.UnitMask.Top); entropyCodingContext.CodedAreaSuperblockUv += blk_geom.BlockWidthUv * blk_geom.BlockHeightUv; } @@ -1041,9 +1041,9 @@ internal partial class Av1TileWriter Av1TransformSize tx_size = blockGeometry.TransformSize[tx_depth]; int coeff1d_offset = entropyCodingContext.CodedAreaSuperblock; - Span coeff_buffer = coeff_ptr.BufferY!.DangerousGetSingleSpan()[coeff1d_offset..]; + Span coeff_buffer = coeff_ptr.GetPlaneBuffer(Av1Plane.Y).DangerousGetSingleSpan()[coeff1d_offset..]; - Av1TransformBlockContext blockContext = new(); + Av1TransformBlockContext blockContext = default; Point transformOrigin = blockGeometry.TransformOrigin[tx_depth][txb_itr]; GetTransformBlockContexts( pcs, @@ -1138,8 +1138,8 @@ internal partial class Av1TileWriter if (blockGeometry.HasUv) { // Both chroma planes share transform geometry but retain independent coefficient contexts. - Span coeff_buffer = coeff_ptr.BufferCb!.DangerousGetSingleSpan().Slice(entropyCodingContext.CodedAreaSuperblockUv); - Av1TransformBlockContext blockContext = new(); + Span coeff_buffer = coeff_ptr.GetPlaneBuffer(Av1Plane.U).DangerousGetSingleSpan().Slice(entropyCodingContext.CodedAreaSuperblockUv); + Av1TransformBlockContext blockContext = default; Point transformOrigin = blockGeometry.TransformOrigin[tx_depth][tx_index]; GetTransformBlockContexts( pcs, @@ -1162,8 +1162,8 @@ internal partial class Av1TileWriter frameHeader.UseReducedTransformSet, blk_ptr.FilterIntraMode); - coeff_buffer = coeff_ptr.BufferCr!.DangerousGetSingleSpan().Slice(entropyCodingContext.CodedAreaSuperblockUv); - blockContext = new(); + coeff_buffer = coeff_ptr.GetPlaneBuffer(Av1Plane.V).DangerousGetSingleSpan().Slice(entropyCodingContext.CodedAreaSuperblockUv); + blockContext = default; int endOfBlockCr = blk_ptr.TransformBlocks[tx_index].NzCoefficientCount[2]; GetTransformBlockContexts( diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TransformBlockContext.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TransformBlockContext.cs index 0f15b2231..ac7fedf60 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TransformBlockContext.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TransformBlockContext.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; /// /// Carries the neighboring coefficient contexts used to entropy-code an AV1 transform block. /// -internal class Av1TransformBlockContext +internal struct Av1TransformBlockContext { /// /// Gets or sets the context used to decode the sign of the DC coefficient. diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs index 4f46ac8c5..e424f8d32 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs @@ -14,6 +14,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; @@ -48,9 +49,9 @@ internal sealed class Av1BlockDecoder : IDisposable private readonly Av1InverseQuantizer inverseQuantizer; /// - /// The retained reconstructed frames addressable by inter prediction, when reconstruction belongs to a decoder session. + /// The retained reconstructed frames addressable by inter prediction. /// - private readonly Av1ReferenceFrameStore? referenceFrames; + private readonly Av1ReferenceFrameStore referenceFrames; /// /// Owns the reusable raster-order inverse-quantization buffer. @@ -67,6 +68,11 @@ internal sealed class Av1BlockDecoder : IDisposable /// private readonly IMemoryOwner predictionScratchOwner; + /// + /// The reusable predictor portion of , excluding compound and chroma-from-luma storage. + /// + private readonly int predictorWorkingLength; + /// /// Reconstructs intra-predicted blocks using the frame-owned prediction workspace. /// @@ -80,7 +86,7 @@ internal sealed class Av1BlockDecoder : IDisposable /// /// The next packed coefficient position for each plane in the current superblock. /// - private readonly int[] currentCoefficientIndex; + private InlineArray4 currentCoefficientIndex; /// /// Accumulates reconstructed luma samples until a chroma-from-luma prediction block can consume them. @@ -95,14 +101,16 @@ internal sealed class Av1BlockDecoder : IDisposable /// The frame buffer receiving reconstructed samples. /// The transform-size map populated while reconstructing blocks. /// The inverse quantizer carrying the active superblock delta-Q state. - /// The retained reconstructed frames selected by inter blocks, or for intra-only use. + /// The retained reconstructed frames selected by inter blocks. + /// The complete decoder-session palette map state. public Av1BlockDecoder( ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, Av1FrameBuffer frameBuffer, Av1LoopFilterContext loopFilterContext, Av1InverseQuantizer inverseQuantizer, - Av1ReferenceFrameStore? referenceFrames = null) + Av1ReferenceFrameStore referenceFrames, + Av1TileReader.PaletteColorIndexMaps? paletteColorIndexMaps = null) { this.sequenceHeader = sequenceHeader; this.frameHeader = frameHeader; @@ -130,28 +138,34 @@ internal sealed class Av1BlockDecoder : IDisposable int predictorWorkingLength = Math.Max( Av1PredictionDecoder.ScratchLength, Math.Max( - Av1InterPredictor.GetScratchLength(maximumBlockLength, maximumBlockLength), + Av1TranslationalInterPredictor.GetScratchLength(maximumBlockLength, maximumBlockLength), Av1ScaledInterPredictor.GetMaximumScaledScratchLength(maximumBlockLength, maximumBlockLength))); int compoundMaskLength = (maximumBlockArea + 1) >> 1; + int predictorWorkingOffset = (2 * maximumBlockArea) + compoundMaskLength; + int chromaFromLumaOffset = predictorWorkingOffset + predictorWorkingLength; // Compound prediction retains both high-precision reference planes plus the full-resolution luma mask. - // Keeping all three beside the convolution workspace lets chroma reuse the luma mask without a copy or rent. - int predictionScratchLength = (2 * maximumBlockArea) + compoundMaskLength + predictorWorkingLength; + // Keeping those planes, convolution workspace, and CfL surface in one owner avoids independent managed + // buffers while ensuring the two scratch consumers never overlap. + int predictionScratchLength = chromaFromLumaOffset + Av1ChromaFromLumaContext.BufferLength; predictionScratchOwner = this.frameBuffer.MemoryAllocator.Allocate(predictionScratchLength); this.inverseQuantizationOwner = inverseQuantizationOwner; this.transformWorkspaceOwner = transformWorkspaceOwner; this.predictionScratchOwner = predictionScratchOwner; + this.predictorWorkingLength = predictorWorkingLength; this.predictionDecoder = new( sequenceHeader, frameHeader, - predictionScratchOwner.Memory[((2 * maximumBlockArea) + compoundMaskLength)..]); + predictionScratchOwner.Memory.Slice(predictorWorkingOffset, predictorWorkingLength), + paletteColorIndexMaps); this.isLoopFilterEnabled = frameHeader.LoopFilterParameters.FilterLevel[0] != 0 || frameHeader.LoopFilterParameters.FilterLevel[1] != 0; - this.currentCoefficientIndex = new int[3]; - this.chromaFromLumaContext = new(sequenceHeader.ColorConfig); + this.chromaFromLumaContext = new( + sequenceHeader.ColorConfig, + predictionScratchOwner.Memory.Slice(chromaFromLumaOffset, Av1ChromaFromLumaContext.BufferLength)); } catch { @@ -245,15 +259,15 @@ internal sealed class Av1BlockDecoder : IDisposable : modeInfo.GetTransformUnitCount(Av1Plane.U); bool isInterBlock = modeInfo.ReferenceFrames[0] >= Av1ReferenceFrameType.Last; - Av1FrameBuffer? referenceFrameBuffer = null; - Av1FrameBuffer? secondaryReferenceFrameBuffer = null; + InterReferenceBuffers? interReferenceBuffers = null; bool isCompound = modeInfo.ReferenceFrames[1] > Av1ReferenceFrameType.Intra; bool isInterIntra = modeInfo.ReferenceFrames[1] == Av1ReferenceFrameType.Intra; int firstCompoundWeight = 8; int secondCompoundWeight = 8; if (isInterBlock) { - referenceFrameBuffer = this.ResolveReferenceFrame(modeInfo.ReferenceFrames[0]); + Av1FrameBuffer primaryReferenceFrameBuffer = this.ResolveReferenceFrame(modeInfo.ReferenceFrames[0]); + Av1FrameBuffer secondaryReferenceFrameBuffer = primaryReferenceFrameBuffer; if (isCompound) { secondaryReferenceFrameBuffer = this.ResolveReferenceFrame(modeInfo.ReferenceFrames[1]); @@ -268,6 +282,10 @@ internal sealed class Av1BlockDecoder : IDisposable out secondCompoundWeight); } } + + // A non-compound block aliases the unused secondary slot to its required primary frame. This keeps the + // published inter state complete without manufacturing a nullable second half. + interReferenceBuffers = new(primaryReferenceFrameBuffer, secondaryReferenceFrameBuffer); } bool highBitDepth = this.frameBuffer.BytesPerSample == 2; @@ -285,9 +303,9 @@ internal sealed class Av1BlockDecoder : IDisposable // following the U descriptors for this block, so the V base includes the complete U transform-unit count. int transformInfoIndex = plane switch { - 2 => superblockInfo.TransformInfoIndexUv + modeInfo.GetFirstTransformLocation(Av1Plane.V) + chromaTransformUnitCount, - 1 => superblockInfo.TransformInfoIndexUv + modeInfo.GetFirstTransformLocation(Av1Plane.U), - 0 => superblockInfo.TransformInfoIndexY + modeInfo.GetFirstTransformLocation(Av1Plane.Y), + 2 => modeInfo.GetFirstTransformLocation(Av1Plane.V) + chromaTransformUnitCount, + 1 => modeInfo.GetFirstTransformLocation(Av1Plane.U), + 0 => modeInfo.GetFirstTransformLocation(Av1Plane.Y), _ => throw new InvalidImageContentException("Maximum of 3 color planes") }; Span transformInfo = superblockInfo.GetTransformInfo(plane)[transformInfoIndex..]; @@ -323,8 +341,11 @@ internal sealed class Av1BlockDecoder : IDisposable blockReconstructionBuffer = this.frameBuffer.DeriveBlockPointer((Av1Plane)plane, pixelPosition, subX, subY, out reconstructionStride); } - if (isInterBlock) + if (interReferenceBuffers is not null) { + InterReferenceBuffers referenceBuffers = interReferenceBuffers.Value; + Av1FrameBuffer primaryReferenceFrameBuffer = referenceBuffers.Primary; + int predictionWidth = Math.Max(4, blockSize.GetWidth() >> subX); int predictionHeight = Math.Max(4, blockSize.GetHeight() >> subY); int maximumBlockLength = 1 << this.sequenceHeader.SuperblockSizeLog2; @@ -339,7 +360,9 @@ internal sealed class Av1BlockDecoder : IDisposable predictionStorage.Slice(2 * maximumBlockArea, compoundMaskStorageLength))[..(blockSize.GetWidth() * blockSize.GetHeight())]; Span predictionScratch = - predictionStorage[((2 * maximumBlockArea) + compoundMaskStorageLength)..]; + predictionStorage.Slice( + (2 * maximumBlockArea) + compoundMaskStorageLength, + this.predictorWorkingLength); Span secondPrediction = MemoryMarshal.AsBytes(secondPredictionStorage)[..(predictionWidth * predictionHeight)]; Span highBitDepthSecondPrediction = MemoryMarshal.Cast(secondPredictionStorage)[..(predictionWidth * predictionHeight)]; @@ -380,8 +403,8 @@ internal sealed class Av1BlockDecoder : IDisposable for (int referenceIndex = 0; referenceIndex < referenceCount; referenceIndex++) { Av1FrameBuffer activeReferenceFrameBuffer = referenceIndex == 0 - ? referenceFrameBuffer! - : secondaryReferenceFrameBuffer!; + ? referenceBuffers.Primary + : referenceBuffers.Secondary; Av1MotionVector motionVector = modeInfo.MotionVectors[referenceIndex]; int destinationStride = useCompoundIntermediates @@ -663,7 +686,7 @@ internal sealed class Av1BlockDecoder : IDisposable ? MemoryMarshal.Cast(highBitDepthBlockReconstructionBuffer[reconstructionStride..]) : highBitDepthSecondPrediction; - Av1InterPredictor.Predict( + Av1TranslationalInterPredictor.Predict( source, sourceStride, sourceIndex, @@ -717,7 +740,7 @@ internal sealed class Av1BlockDecoder : IDisposable ? blockReconstructionBuffer[reconstructionStride..] : secondPrediction; - Av1InterPredictor.Predict( + Av1TranslationalInterPredictor.Predict( source, sourceStride, sourceIndex, @@ -1567,7 +1590,7 @@ internal sealed class Av1BlockDecoder : IDisposable Span destination = MemoryMarshal.Cast( highBitDepthBlockReconstructionBuffer[destinationOffset..]); - Av1InterPredictor.Predict( + Av1TranslationalInterPredictor.Predict( source, sourceStride, sourceIndex, @@ -1596,7 +1619,7 @@ internal sealed class Av1BlockDecoder : IDisposable sourceOrigin.X + (sourceColumnQ4 >> 4); - Av1InterPredictor.Predict( + Av1TranslationalInterPredictor.Predict( source, sourceStride, sourceIndex, @@ -1631,7 +1654,7 @@ internal sealed class Av1BlockDecoder : IDisposable // The uncompressed-header parser validates each selected slot and the reference store remains unchanged // until frame reconstruction completes, so every parsed inter block resolves the same retained owner. - return this.referenceFrames!.Resolve((int)referenceSlot)!.FrameBuffer; + return this.referenceFrames.ResolveRequired((int)referenceSlot).FrameBuffer; } /// @@ -2061,7 +2084,7 @@ internal sealed class Av1BlockDecoder : IDisposable int sourceIndex = ((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) + sourceOrigin.X + (sourceColumnQ4 >> 4); - Av1InterPredictor.Predict( + Av1TranslationalInterPredictor.Predict( source, sourceStride, sourceIndex, @@ -2088,7 +2111,7 @@ internal sealed class Av1BlockDecoder : IDisposable int sourceIndex = ((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) + sourceOrigin.X + (sourceColumnQ4 >> 4); - Av1InterPredictor.Predict( + Av1TranslationalInterPredictor.Predict( source, sourceStride, sourceIndex, @@ -2110,86 +2133,6 @@ internal sealed class Av1BlockDecoder : IDisposable private static bool IsOverlappable(Av1BlockModeInfo candidate) => candidate.UseIntraBlockCopy || candidate.ReferenceFrames[0] > Av1ReferenceFrameType.Intra; - /// - /// Derives a byte-addressed reconstruction span beginning one row before a block. - /// - /// The frame buffer containing the destination planes. - /// The zero-based Y, U, or V plane index. - /// The horizontal block origin in plane samples. - /// The vertical block origin in plane samples. - /// The resulting span beginning one row before the block. - /// The number of logical samples between rows. - /// The chroma horizontal subsampling shift. - /// The chroma vertical subsampling shift. - private static void DeriveBlockPointers( - Av1FrameBuffer frameBuffer, - int plane, - int blockColumnInPixels, - int blockRowInPixels, - out Span blockReconstructionBuffer, - out int reconstructionStride, - int subX, - int subY) - { - int blockOffset; - - switch (plane) - { - case 0: - reconstructionStride = frameBuffer.BufferY!.Width; - blockOffset = ((frameBuffer.OriginY + blockRowInPixels) * reconstructionStride) + - (frameBuffer.OriginX + blockColumnInPixels); - break; - case 1: - reconstructionStride = frameBuffer.BufferCb!.Width; - blockOffset = (((frameBuffer.OriginY >> subY) + blockRowInPixels) * reconstructionStride) + - ((frameBuffer.OriginX >> subX) + blockColumnInPixels); - break; - default: - reconstructionStride = frameBuffer.BufferCr!.Width; - blockOffset = (((frameBuffer.OriginY >> subY) + blockRowInPixels) * reconstructionStride) + - ((frameBuffer.OriginX >> subX) + blockColumnInPixels); - break; - } - - // Prediction addresses above samples relative to the returned span, so expose the previous row as index zero. - blockOffset -= reconstructionStride; - Guard.MustBeGreaterThanOrEqualTo(blockOffset, 0, nameof(blockOffset)); - - if (frameBuffer.BitDepth != Av1BitDepth.EightBit || frameBuffer.Is16BitPipeline) - { - // The legacy byte view represents each high-bit-depth sample with two adjacent storage elements. - blockOffset *= 2; - if (plane == 0) - { - blockReconstructionBuffer = frameBuffer.BufferY!.DangerousGetSingleSpan()[blockOffset..]; - } - else if (plane == 1) - { - blockReconstructionBuffer = frameBuffer.BufferCb!.DangerousGetSingleSpan()[blockOffset..]; - } - else - { - blockReconstructionBuffer = frameBuffer.BufferCr!.DangerousGetSingleSpan()[blockOffset..]; - } - } - else - { - if (plane == 0) - { - blockReconstructionBuffer = frameBuffer.BufferY!.DangerousGetSingleSpan()[blockOffset..]; - } - else if (plane == 1) - { - blockReconstructionBuffer = frameBuffer.BufferCb!.DangerousGetSingleSpan()[blockOffset..]; - } - else - { - blockReconstructionBuffer = frameBuffer.BufferCr!.DangerousGetSingleSpan()[blockOffset..]; - } - } - } - /// /// Determines whether reconstructed luma samples must be retained for a later chroma-from-luma prediction. /// @@ -2201,4 +2144,29 @@ internal sealed class Av1BlockDecoder : IDisposable private static bool StoreChromaFromLumaRequired(ObuColorConfig colorConfig, ref Av1PartitionInfo partitionInfo) => !colorConfig.IsMonochrome && (!partitionInfo.IsChroma || partitionInfo.ModeInfo.UvMode == Av1ChromaPredictionMode.ChromaFromLuma); + + /// + /// Carries a complete pair of retained buffers through the inter-only reconstruction branch. + /// + private readonly struct InterReferenceBuffers + { + /// + /// Initializes a new instance of the struct. + /// + public InterReferenceBuffers(Av1FrameBuffer primary, Av1FrameBuffer secondary) + { + this.Primary = primary; + this.Secondary = secondary; + } + + /// + /// Gets the primary retained frame. + /// + public Av1FrameBuffer Primary { get; } + + /// + /// Gets the secondary retained frame. + /// + public Av1FrameBuffer Secondary { get; } + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.ByteOutputOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.ByteOutputOperator.cs index cec05507d..8d9a71d80 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.ByteOutputOperator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.ByteOutputOperator.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; /// /// Defines eight-bit inverse-transform reconstruction arithmetic. /// -internal partial class Av1InverseTransformer +internal static partial class Av1InverseTransformer { /// /// Reconstructs eight-bit samples from predicted values and inverse-transform residuals. diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.HighBitDepthOutputOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.HighBitDepthOutputOperator.cs index 19eb1c4a6..2bfb5b5c0 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.HighBitDepthOutputOperator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.HighBitDepthOutputOperator.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; /// /// Defines high-bit-depth inverse-transform reconstruction arithmetic. /// -internal partial class Av1InverseTransformer +internal static partial class Av1InverseTransformer { /// /// Reconstructs high-bit-depth samples from predicted values and inverse-transform residuals. diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.Operator.cs index c0b7f9159..1ce7495ae 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.Operator.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.Operator.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; /// /// Defines the inverse-transform reconstruction operator contract. /// -internal partial class Av1InverseTransformer +internal static partial class Av1InverseTransformer { /// /// Defines how inverse-transform residuals are added to decoded samples. diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.cs index 1c72acbc0..852ac8e73 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; /// /// Reconstructs decoded AV1 transform coefficients into prediction sample buffers. /// -internal partial class Av1InverseTransformer +internal static partial class Av1InverseTransformer { /// /// Reconstructs an eight-bit transform block in place by adding its inverse-transform residual. diff --git a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs index 90c57e1ac..eb496c9f2 100644 --- a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Heif; /// Decodes a single AV1-coded HEIF image item. /// /// The destination pixel type. -internal class Av1HeifItemDecoder : IHeifItemDecoder, IHeifAlphaItemDecoder +internal sealed class Av1HeifItemDecoder : IHeifItemDecoder, IHeifAlphaItemDecoder where TPixel : unmanaged, IPixel { /// diff --git a/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs index 2aa83e144..8f10ef45a 100644 --- a/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs +++ b/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs @@ -16,13 +16,13 @@ namespace SixLabors.ImageSharp.Formats.Heif; /// Decodes the image items referenced by a HEIF grid derived-image item. /// /// The destination pixel type. -internal class GridHeifItemDecoder : IHeifItemDecoder, IHeifAlphaItemDecoder +internal sealed class GridHeifItemDecoder : IHeifItemDecoder, IHeifAlphaItemDecoder where TPixel : unmanaged, IPixel { /// - /// The item definitions available to the grid. + /// The item definitions available to the grid, indexed by item identifier. /// - private readonly IList items; + private readonly Dictionary items; /// /// The item-reference relationships used to locate the grid's tiles. @@ -30,9 +30,9 @@ internal class GridHeifItemDecoder : IHeifItemDecoder, IHeifAlph private readonly IList itemLinks; /// - /// The assembled encoded payload for each referenced image item. + /// Reads one selected encoded item payload on demand. /// - private readonly IDictionary> buffers; + private readonly Func> itemDataReader; /// /// The optional row-major tile identifiers supplied for an auxiliary grid plane. @@ -44,19 +44,25 @@ internal class GridHeifItemDecoder : IHeifItemDecoder, IHeifAlph /// /// The item definitions in the containing HEIF file. /// The item-reference relationships in the containing HEIF file. - /// The assembled encoded payload for each image item. + /// Reads one selected encoded image payload on demand. /// /// Optional row-major tile identifiers that replace the grid item's own derived-image references. /// public GridHeifItemDecoder( IList items, IList itemLinks, - IDictionary> buffers, + Func> itemDataReader, IReadOnlyList? tileItemIds = null) { - this.items = items; + Dictionary itemLookup = new(items.Count); + foreach (HeifItem item in items) + { + itemLookup.Add(item.Id, item); + } + + this.items = itemLookup; this.itemLinks = itemLinks; - this.buffers = buffers; + this.itemDataReader = itemDataReader; this.tileItemIds = tileItemIds; } @@ -87,93 +93,175 @@ internal class GridHeifItemDecoder : IHeifItemDecoder, IHeifAlph CancellationToken cancellationToken) { GridDescriptor descriptor = ParseGridDescriptor(data); - int rows = descriptor.Rows; - int columns = descriptor.Columns; - int outputWidth = descriptor.OutputSize.Width; - int outputHeight = descriptor.OutputSize.Height; - List linked = this.GetLinkedTileIds(gridItem, descriptor); - - // Each compressed tile decoder returns an owned Image. Keep every tile alive until - // the final grid has copied its pixels, then dispose all intermediates together. - using DisposableList> gridTiles = new(linked.Count); + IReadOnlyList linked = this.GetLinkedTileIds(gridItem, descriptor); + Heif4CharCode tileType = default; Av1CodecConfiguration? av1GridConfiguration = null; - foreach (uint id in linked) + Image result = this.CreateGridResult( + options, + descriptor, + linked[0], + colorProfile, + ref tileType, + ref av1GridConfiguration, + cancellationToken, + out int tileWidth, + out int tileHeight); + + try { - cancellationToken.ThrowIfCancellationRequested(); - HeifItem item = this.items.First(item => item.Id == id); - ValidateTileConfiguration(item, ref tileType, ref av1GridConfiguration); - - IHeifItemDecoder? decoder = HeifCompressionFactory.GetDecoder(item.Type) - ?? throw new ImageFormatException($"The HEIF image grid uses unsupported tile type '{item.Type}'."); - - if (!this.buffers.TryGetValue(item.Id, out IMemoryOwner? itemMemory)) + for (int tileIndex = 1; tileIndex < linked.Count; tileIndex++) { - throw new InvalidImageContentException($"HEIF image grid tile {item.Id} has no data extents."); - } - - this.CompressionMethod = decoder.CompressionMethod; - Image tile = decoder.DecodeItemData( - options, - item, - itemMemory.GetSpan(), - item.CicpProfile ?? colorProfile, - cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + HeifItem item = this.items[linked[tileIndex]]; + using Image tile = this.DecodeGridTile( + options, + item, + colorProfile, + ref tileType, + ref av1GridConfiguration, + cancellationToken); + + if (tile.Width != tileWidth || tile.Height != tileHeight) + { + throw new InvalidImageContentException("The HEIF image grid contains tiles with mismatched dimensions."); + } - try - { - HeifItemDecoderUtilities.ScaleToItemExtent(tile, item); - gridTiles.Add(tile); - } - catch - { - tile.Dispose(); - throw; + CopyGridTile(tile, result, descriptor, tileIndex, tileWidth, tileHeight); } + + return result; + } + catch + { + result.Dispose(); + throw; } + } - Image firstTile = gridTiles[0]; - int tileWidth = firstTile.Width; - int tileHeight = firstTile.Height; - if (((long)tileWidth * columns) < outputWidth || ((long)tileHeight * rows) < outputHeight) + /// + /// Decodes the first validated grid tile, establishes the common tile geometry, and creates the output canvas. + /// + private Image CreateGridResult( + DecoderOptions options, + in GridDescriptor descriptor, + uint firstTileId, + CicpProfile? colorProfile, + ref Heif4CharCode tileType, + ref Av1CodecConfiguration? av1GridConfiguration, + CancellationToken cancellationToken, + out int tileWidth, + out int tileHeight) + { + cancellationToken.ThrowIfCancellationRequested(); + HeifItem item = this.items[firstTileId]; + using Image tile = this.DecodeGridTile( + options, + item, + colorProfile, + ref tileType, + ref av1GridConfiguration, + cancellationToken); + + tileWidth = tile.Width; + tileHeight = tile.Height; + if (((long)tileWidth * descriptor.Columns) < descriptor.OutputSize.Width || + ((long)tileHeight * descriptor.Rows) < descriptor.OutputSize.Height) { throw new InvalidImageContentException("The HEIF image grid tiles do not cover the output canvas."); } - if (((long)tileWidth * (columns - 1)) >= outputWidth || ((long)tileHeight * (rows - 1)) >= outputHeight) + if (((long)tileWidth * (descriptor.Columns - 1)) >= descriptor.OutputSize.Width || + ((long)tileHeight * (descriptor.Rows - 1)) >= descriptor.OutputSize.Height) { throw new InvalidImageContentException("The HEIF image grid edge tiles do not overlap the output canvas."); } - Image result = new(options.Configuration, outputWidth, outputHeight, firstTile.Metadata.DeepClone()); - ImageFrame destination = result.Frames.RootFrame; - for (int tileIndex = 0; tileIndex < gridTiles.Count; tileIndex++) + Image result = new( + options.Configuration, + descriptor.OutputSize.Width, + descriptor.OutputSize.Height, + tile.Metadata.DeepClone()); + + try { - Image tile = gridTiles[tileIndex]; - if (tile.Width != tileWidth || tile.Height != tileHeight) - { - result.Dispose(); - throw new InvalidImageContentException("The HEIF image grid contains tiles with mismatched dimensions."); - } + CopyGridTile(tile, result, descriptor, 0, tileWidth, tileHeight); + return result; + } + catch + { + result.Dispose(); + throw; + } + } - int column = tileIndex % columns; - int row = tileIndex / columns; - int destinationX = column * tileWidth; - int destinationY = row * tileHeight; - int copyWidth = Math.Min(tileWidth, outputWidth - destinationX); - int copyHeight = Math.Min(tileHeight, outputHeight - destinationY); - ImageFrame source = tile.Frames.RootFrame; - - // The descriptor may crop only the rightmost column and bottom row. Copying bounded row spans - // applies that crop without allocating derived-image views or invoking the processing pipeline. - for (int y = 0; y < copyHeight; y++) - { - Span destinationRow = destination.PixelBuffer.DangerousGetRowSpan(destinationY + y).Slice(destinationX, copyWidth); - source.PixelBuffer.DangerousGetRowSpan(y)[..copyWidth].CopyTo(destinationRow); - } + /// + /// Decodes and scales one grid tile while its encoded payload owner is active. + /// + private Image DecodeGridTile( + DecoderOptions options, + HeifItem item, + CicpProfile? colorProfile, + ref Heif4CharCode tileType, + ref Av1CodecConfiguration? av1GridConfiguration, + CancellationToken cancellationToken) + { + ValidateTileConfiguration(item, ref tileType, ref av1GridConfiguration); + IHeifItemDecoder? decoder = HeifCompressionFactory.GetDecoder(item.Type) + ?? throw new ImageFormatException($"The HEIF image grid uses unsupported tile type '{item.Type}'."); + + using IMemoryOwner itemMemory = this.itemDataReader(item); + this.CompressionMethod = decoder.CompressionMethod; + Image tile = decoder.DecodeItemData( + options, + item, + itemMemory.GetSpan(), + item.CicpProfile ?? colorProfile, + cancellationToken); + + try + { + HeifItemDecoderUtilities.ScaleToItemExtent(tile, item); + return tile; + } + catch + { + // Ownership transfers to the caller only after extent normalization succeeds. + tile.Dispose(); + throw; } + } - return result; + /// + /// Copies one decoded tile into its cropped row-major grid position. + /// + private static void CopyGridTile( + Image tile, + Image result, + in GridDescriptor descriptor, + int tileIndex, + int tileWidth, + int tileHeight) + { + int column = tileIndex % descriptor.Columns; + int row = tileIndex / descriptor.Columns; + int destinationX = column * tileWidth; + int destinationY = row * tileHeight; + int copyWidth = Math.Min(tileWidth, descriptor.OutputSize.Width - destinationX); + int copyHeight = Math.Min(tileHeight, descriptor.OutputSize.Height - destinationY); + ImageFrame source = tile.Frames.RootFrame; + ImageFrame destination = result.Frames.RootFrame; + + // Copy before disposing this decoded tile, keeping peak tile storage independent of grid cell count. + // The descriptor may crop only the rightmost column and bottom row. + for (int y = 0; y < copyHeight; y++) + { + Span destinationRow = destination.PixelBuffer + .DangerousGetRowSpan(destinationY + y) + .Slice(destinationX, copyWidth); + + source.PixelBuffer.DangerousGetRowSpan(y)[..copyWidth].CopyTo(destinationRow); + } } /// @@ -188,7 +276,7 @@ internal class GridHeifItemDecoder : IHeifItemDecoder, IHeifAlph CancellationToken cancellationToken) { GridDescriptor descriptor = ParseGridDescriptor(data); - List linked = this.GetLinkedTileIds(gridItem, descriptor); + IReadOnlyList linked = this.GetLinkedTileIds(gridItem, descriptor); Heif4CharCode tileType = default; Av1CodecConfiguration? av1GridConfiguration = null; Size tileSize = default; @@ -197,18 +285,13 @@ internal class GridHeifItemDecoder : IHeifItemDecoder, IHeifAlph // grid without leaving a partially composed prefix in the returned image. foreach (uint id in linked) { - HeifItem item = this.items.First(item => item.Id == id); + HeifItem item = this.items[id]; ValidateTileConfiguration(item, ref tileType, ref av1GridConfiguration); if (HeifCompressionFactory.GetDecoder(item.Type) is not IHeifAlphaItemDecoder) { throw new ImageFormatException($"The HEIF alpha grid uses unsupported tile type '{item.Type}'."); } - if (!this.buffers.ContainsKey(item.Id)) - { - throw new InvalidImageContentException($"HEIF alpha grid tile {item.Id} has no data extents."); - } - if (item.Extent == default) { throw new InvalidImageContentException($"HEIF alpha grid tile {item.Id} has no spatial extent."); @@ -245,9 +328,14 @@ internal class GridHeifItemDecoder : IHeifItemDecoder, IHeifAlph for (int tileIndex = 0; tileIndex < linked.Count; tileIndex++) { cancellationToken.ThrowIfCancellationRequested(); - HeifItem item = this.items.First(item => item.Id == linked[tileIndex]); - IHeifAlphaItemDecoder decoder = (IHeifAlphaItemDecoder)HeifCompressionFactory.GetDecoder(item.Type)!; - IMemoryOwner itemMemory = this.buffers[item.Id]; + HeifItem item = this.items[linked[tileIndex]]; + IHeifItemDecoder? itemDecoder = HeifCompressionFactory.GetDecoder(item.Type); + if (itemDecoder is not IHeifAlphaItemDecoder decoder) + { + throw new InvalidImageContentException($"HEIF alpha grid tile {item.Id} uses unsupported coding format '{item.Type}'."); + } + + using IMemoryOwner itemMemory = this.itemDataReader(item); int column = tileIndex % descriptor.Columns; int row = tileIndex / descriptor.Columns; int destinationX = destinationRectangle.X + (column * tileSize.Width); @@ -315,19 +403,27 @@ internal class GridHeifItemDecoder : IHeifItemDecoder, IHeifAlph /// The grid item whose derived-image references are being resolved. /// The validated grid dimensions. /// The exact row-major tile identifiers required by the descriptor. - private List GetLinkedTileIds(HeifItem gridItem, in GridDescriptor descriptor) + private IReadOnlyList GetLinkedTileIds(HeifItem gridItem, in GridDescriptor descriptor) { - List linked = this.tileItemIds is null ? [] : new(this.tileItemIds); - if (this.tileItemIds is null) + IReadOnlyList linked; + if (this.tileItemIds is not null) + { + // Auxiliary grids already own an immutable row-major identifier list; no defensive list copy is needed. + linked = this.tileItemIds; + } + else { + List resolved = []; foreach (HeifItemLink link in this.itemLinks) { if (link.Type == Heif4CharCode.Dimg && link.SourceId == gridItem.Id) { // The order of dimg destinations is the normative row-major order of the grid cells. - linked.AddRange(link.DestinationIds); + resolved.AddRange(link.DestinationIds); } } + + linked = resolved; } int tileCount = descriptor.Rows * descriptor.Columns; diff --git a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs index 7c5d70bff..0cf3db3dd 100644 --- a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs +++ b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs @@ -746,7 +746,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore foreach (uint tileId in link.DestinationIds) { - HeifItem tile = this.FindItemById(tileId)!; + HeifItem tile = this.FindRequiredItemById(tileId); if (tile.Type != gridTile.Type) { throw new InvalidImageContentException("All HEIF image grid tiles must use the same coding format."); @@ -987,22 +987,22 @@ internal sealed class HeifDecoderCore : ImageDecoderCore item.ContentEncoding = ReadNullTerminatedString(entryBuffer[bytesRead..], out int contentEncodingLength); bytesRead += contentEncodingLength; } - } - if (version == 1) - { - if (bytesRead < totalLength) + if (version == 1) { - EnsureBufferRemaining(entryBuffer, bytesRead, 4, "item info entry"); - item!.ExtensionType = BinaryPrimitives.ReadUInt32BigEndian(entryBuffer[bytesRead..]); - bytesRead += 4; - } + if (bytesRead < totalLength) + { + EnsureBufferRemaining(entryBuffer, bytesRead, 4, "item info entry"); + item.ExtensionType = BinaryPrimitives.ReadUInt32BigEndian(entryBuffer[bytesRead..]); + bytesRead += 4; + } - if (bytesRead < totalLength) - { - // Version-one extension payloads are outside the image item types currently - // consumed by this decoder, but remain bounded within this entry. - bytesRead = totalLength; + if (bytesRead < totalLength) + { + // Version-one extension payloads are outside the image item types currently + // consumed by this decoder, but remain bounded within this entry. + bytesRead = totalLength; + } } } @@ -2136,116 +2136,103 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } /// - /// Resolves item extents, selects the primary or supported thumbnail decoder, and reconstructs the image. + /// Assembles one logical item from its declared extents. /// - /// The destination pixel format. /// The complete seekable HEIF container stream. - /// The token used to cancel item assembly and payload decoding. - /// The image reconstructed from the selected item. - private Image DecodePrimaryItem(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + /// The item whose extents are requested. + /// An owner containing the contiguous item payload. + private IMemoryOwner ReadItemData(BufferedReadStream stream, HeifItem item) { - using DisposableDictionary> buffers = new(this.items.Count); - foreach (HeifItem item in this.items) + long itemLength = 0; + foreach (HeifLocation location in item.DataLocations) { - cancellationToken.ThrowIfCancellationRequested(); - bool isMetadataItem = item.Type is Heif4CharCode.Exif or Heif4CharCode.Mime; - if (this.Options.SkipMetadata && isMetadataItem) + if (location.Length < 0 || itemLength > int.MaxValue - location.Length) { - // Metadata items are not codec inputs. Leave their extents on the stream when metadata loading is disabled. - continue; + throw new InvalidImageContentException($"Item {item.Id} data is too large to buffer."); } - IMemoryOwner? extentMemory = null; - try + itemLength += location.Length; + } + + if (itemLength == 0) + { + throw new InvalidImageContentException($"Item {item.Id} has no data extents."); + } + + int bufferLength = (int)itemLength; + IMemoryOwner itemMemory = this.configuration.MemoryAllocator.Allocate(bufferLength); + try + { + // One logical item is the concatenation of its extents in declared order. Only the selected item is + // materialized, and its owner is released as soon as the codec or metadata consumer has finished. + Span itemBuffer = itemMemory.GetSpan()[..bufferLength]; + int writeOffset = 0; + foreach (HeifLocation location in item.DataLocations) { - long itemLength = 0; - foreach (HeifLocation loc in item.DataLocations) + if (location.BaseOffset < 0 + || location.Offset < 0 + || location.BaseOffset > long.MaxValue - location.Offset) { - if (loc.Length < 0 || itemLength > int.MaxValue - loc.Length) - { - throw new InvalidImageContentException($"Item {item.Id} data is too large to buffer."); - } - - itemLength += loc.Length; + throw new InvalidImageContentException($"Item {item.Id} has an invalid extent offset."); } - if (itemLength == 0) + long relativeOffset = location.BaseOffset + location.Offset; + long sourceOffset; + long sourceBytesRemaining; + if (location.Origin == HeifLocationOffsetOrigin.FileOffset) { - continue; + // Construction method zero resolves base_offset + extent_offset from the start of the file. + sourceOffset = relativeOffset; + sourceBytesRemaining = stream.Length - sourceOffset; } - - // One logical item is the concatenation of its extents in declared order. Materialize only that item data, - // never the enclosing file or mdat box, so codec readers receive the contiguous payload they expect. - int bufferLength = (int)itemLength; - extentMemory = this.configuration.MemoryAllocator.Allocate(bufferLength); - Span itemBuffer = extentMemory.GetSpan()[..bufferLength]; - int writeOffset = 0; - foreach (HeifLocation loc in item.DataLocations) + else if (location.Origin == HeifLocationOffsetOrigin.ItemDataOffset) { - if (loc.BaseOffset < 0 || loc.Offset < 0 || loc.BaseOffset > long.MaxValue - loc.Offset) - { - throw new InvalidImageContentException($"Item {item.Id} has an invalid extent offset."); - } - - long relativeOffset = loc.BaseOffset + loc.Offset; - long sourceOffset; - long sourceBytesRemaining; - if (loc.Origin == HeifLocationOffsetOrigin.FileOffset) + if (this.itemDataOffset < 0 || relativeOffset > this.itemDataLength) { - // Construction method zero resolves base_offset + extent_offset from the start of the file. - sourceOffset = relativeOffset; - sourceBytesRemaining = stream.Length - sourceOffset; + throw new InvalidImageContentException($"Item {item.Id} has an extent outside its item data box."); } - else if (loc.Origin == HeifLocationOffsetOrigin.ItemDataOffset) - { - if (this.itemDataOffset < 0 || relativeOffset > this.itemDataLength) - { - throw new InvalidImageContentException($"Item {item.Id} has an extent outside its item data box."); - } - // Construction method one resolves the same relative value from the idat payload start. - sourceOffset = this.itemDataOffset + relativeOffset; - sourceBytesRemaining = this.itemDataLength - relativeOffset; - } - else - { - throw new InvalidImageContentException($"Item {item.Id} uses an unsupported location origin."); - } - - HeifBoxReader.EnsureInsideParent(loc.Length, sourceBytesRemaining); - stream.Position = sourceOffset; - int extentLength = (int)loc.Length; - int bytesRead = stream.Read(itemBuffer.Slice(writeOffset, extentLength)); - if (bytesRead != extentLength) - { - throw new InvalidImageContentException($"Item {item.Id} extent is truncated."); - } + // Construction method one resolves the same relative value from the idat payload start. + sourceOffset = this.itemDataOffset + relativeOffset; + sourceBytesRemaining = this.itemDataLength - relativeOffset; + } + else + { + throw new InvalidImageContentException($"Item {item.Id} uses an unsupported location origin."); + } - writeOffset += extentLength; + HeifBoxReader.EnsureInsideParent(location.Length, sourceBytesRemaining); + stream.Position = sourceOffset; + int extentLength = (int)location.Length; + int bytesRead = stream.Read(itemBuffer.Slice(writeOffset, extentLength)); + if (bytesRead != extentLength) + { + throw new InvalidImageContentException($"Item {item.Id} extent is truncated."); } - buffers.Add(item.Id, extentMemory); - extentMemory = null; - } - catch (Exception ex) when (isMetadataItem && ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) - { - // A failed optional metadata extent is discarded without weakening image-item extent validation. - extentMemory?.Dispose(); - } - catch (Exception ex) when (!isMetadataItem && ImageDecoderCore.ShouldIgnoreImageDataSegmentError(this.Options, ex)) - { - // Keep the item declaration but omit its unreadable payload. The presentation can still use a valid - // thumbnail, omit an auxiliary plane, or reject the file later when no decodable color item remains. - extentMemory?.Dispose(); - } - catch - { - // The dictionary takes ownership only after every declared extent has been assembled successfully. - extentMemory?.Dispose(); - throw; + writeOffset += extentLength; } + + return itemMemory; } + catch + { + itemMemory.Dispose(); + throw; + } + } + + /// + /// Resolves item extents, selects the primary or supported thumbnail decoder, and reconstructs the image. + /// + /// The destination pixel format. + /// The complete seekable HEIF container stream. + /// The token used to cancel item assembly and payload decoding. + /// The image reconstructed from the selected item. + private Image DecodePrimaryItem(BufferedReadStream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Func> itemDataReader = item => this.ReadItemData(stream, item); HeifItem? rootItem = this.FindItemById(this.primaryItem); if (rootItem is null) @@ -2255,12 +2242,12 @@ internal sealed class HeifDecoderCore : ImageDecoderCore Image? image = null; HeifItem itemToDecode = rootItem; - IHeifItemDecoder? itemDecoder = this.GetItemDecoder(rootItem, buffers); + IHeifItemDecoder? itemDecoder = this.GetItemDecoder(rootItem, itemDataReader); bool supportedItemFound = itemDecoder is not null; if (itemDecoder is not null) { this.ExecuteImageDataSegmentAction( - () => image = this.DecodeImageItem(rootItem, itemDecoder, buffers, cancellationToken)); + () => image = this.DecodeImageItem(rootItem, itemDecoder, itemDataReader, cancellationToken)); } if (image is null) @@ -2276,7 +2263,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore { itemToDecode = thumbnailItem; this.ExecuteImageDataSegmentAction( - () => image = this.DecodeImageItem(thumbnailItem, itemDecoder, buffers, cancellationToken)); + () => image = this.DecodeImageItem(thumbnailItem, itemDecoder, itemDataReader, cancellationToken)); } } } @@ -2295,13 +2282,13 @@ internal sealed class HeifDecoderCore : ImageDecoderCore { bool hasAlpha = false; this.ExecuteImageDataSegmentAction( - () => hasAlpha = this.DecodeAlphaPlane(itemToDecode, buffers, image.Frames.RootFrame, cancellationToken)); + () => hasAlpha = this.DecodeAlphaPlane(itemToDecode, itemDataReader, image.Frames.RootFrame, cancellationToken)); if (!this.Options.SkipMetadata) { this.ApplyItemColorMetadata(image.Metadata, itemToDecode); this.ApplyItemHdrMetadata(image.Metadata, itemToDecode); - this.ApplyAssociatedMetadata(image.Metadata, rootItem, buffers); + this.ApplyAssociatedMetadata(image.Metadata, rootItem, itemDataReader); } // MIAF defines crop, rotation, and mirror as presentation operations in that order. Applying the @@ -2483,11 +2470,11 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// /// The decoded image metadata receiving the profiles. /// The color image item described by the metadata links. - /// The assembled payloads for the container's declared items. + /// Reads one associated item payload on demand. private void ApplyAssociatedMetadata( ImageMetadata metadata, HeifItem colorItem, - DisposableDictionary> buffers) + Func> itemDataReader) { foreach (HeifItemLink link in this.itemLinks) { @@ -2497,20 +2484,25 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } HeifItem? metadataItem = this.FindItemById(link.SourceId); - if (metadataItem is null || !buffers.TryGetValue(metadataItem.Id, out IMemoryOwner? itemMemory)) + if (metadataItem is null) { continue; } if (metadataItem.Type == Heif4CharCode.Exif) { - this.ExecuteAncillarySegmentAction(() => ApplyExifProfile(metadata, itemMemory.GetSpan())); + this.ExecuteAncillarySegmentAction(() => + { + using IMemoryOwner itemMemory = itemDataReader(metadataItem); + ApplyExifProfile(metadata, itemMemory.GetSpan()); + }); } else if (metadataItem.Type == Heif4CharCode.Mime && string.Equals(metadataItem.ContentType, "application/rdf+xml", StringComparison.Ordinal)) { this.ExecuteAncillarySegmentAction(() => { + using IMemoryOwner itemMemory = itemDataReader(metadataItem); Span itemData = itemMemory.GetSpan(); // XmpProfile retains its input array after the assembled item buffer is returned to its pool. @@ -2577,12 +2569,14 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// /// The destination pixel format. /// The coded or derived image item. - /// The assembled payloads available to a grid decoder and its tiles. + /// Reads one selected item payload on demand. /// The selected decoder, or when the item cannot be reconstructed. - private IHeifItemDecoder? GetItemDecoder(HeifItem item, DisposableDictionary> buffers) + private IHeifItemDecoder? GetItemDecoder( + HeifItem item, + Func> itemDataReader) where TPixel : unmanaged, IPixel => item.Type == Heif4CharCode.Grid && this.FindDecodableGridTile(item) is not null - ? new GridHeifItemDecoder(this.items, this.itemLinks, buffers) + ? new GridHeifItemDecoder(this.items, this.itemLinks, itemDataReader) : HeifCompressionFactory.GetDecoder(item.Type); /// @@ -2591,21 +2585,17 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// The destination pixel format. /// The image item to decode. /// The decoder selected for the item. - /// The assembled item payloads. + /// Reads the selected item payload. /// The token used to cancel the payload decode. /// The decoded image. private Image DecodeImageItem( HeifItem item, IHeifItemDecoder decoder, - DisposableDictionary> buffers, + Func> itemDataReader, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - if (!buffers.TryGetValue(item.Id, out IMemoryOwner? itemMemory)) - { - throw new InvalidImageContentException($"Item {item.Id} has no data extents."); - } - + using IMemoryOwner itemMemory = itemDataReader(item); Image image = decoder.DecodeItemData( this.payloadOptions, item, @@ -2716,13 +2706,13 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// /// The destination color pixel type. /// The color image item whose alpha plane is requested. - /// The assembled item payloads. + /// Reads one selected item payload on demand. /// The decoded color frame receiving alpha values. /// The token used to cancel the auxiliary payload decode. /// when an auxiliary alpha plane was decoded and composed. private bool DecodeAlphaPlane( HeifItem colorItem, - DisposableDictionary> buffers, + Func> itemDataReader, ImageFrame destination, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel @@ -2746,7 +2736,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore throw new ImageFormatException("The alpha auxiliary image and color image use different presentation transforms."); } - IHeifItemDecoder? itemDecoder = this.GetItemDecoder(alphaItem, buffers); + IHeifItemDecoder? itemDecoder = this.GetItemDecoder(alphaItem, itemDataReader); if (itemDecoder is not IHeifAlphaItemDecoder decoder) { throw new ImageFormatException($"The alpha auxiliary item uses unsupported item type '{alphaItem.Type}'."); @@ -2757,11 +2747,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore && link.SourceId == colorItem.Id && link.DestinationIds.Contains(alphaItem.Id)); - if (!buffers.TryGetValue(alphaItem.Id, out IMemoryOwner? itemMemory)) - { - throw new InvalidImageContentException($"Item {alphaItem.Id} has no data extents."); - } - + using IMemoryOwner itemMemory = itemDataReader(alphaItem); decoder.DecodeAlphaItemData( this.payloadOptions, alphaItem, @@ -2786,19 +2772,15 @@ internal sealed class HeifDecoderCore : ImageDecoderCore return false; } - if (!buffers.TryGetValue(colorItem.Id, out IMemoryOwner? gridMemory)) - { - throw new InvalidImageContentException($"Item {colorItem.Id} has no data extents."); - } - // The color grid descriptor defines the same row/column layout and output canvas for per-tile alpha // auxiliaries. Supplying their IDs lets the existing grid compositor preserve that normative ordering. GridHeifItemDecoder gridDecoder = new( this.items, this.itemLinks, - buffers, + itemDataReader, alphaTileIds); + using IMemoryOwner gridMemory = itemDataReader(colorItem); gridDecoder.DecodeAlphaItemData( this.payloadOptions, colorItem, @@ -2835,6 +2817,16 @@ internal sealed class HeifDecoderCore : ImageDecoderCore private HeifItem? FindItemById(uint itemId) => this.items.FirstOrDefault(item => item.Id == itemId); + /// + /// Resolves an item identifier referenced by another parsed HEIF structure. + /// + /// The required item identifier. + /// The declared item. + /// No item with the referenced identifier was declared. + private HeifItem FindRequiredItemById(uint itemId) + => this.FindItemById(itemId) + ?? throw new InvalidImageContentException($"HEIF item reference targets undeclared item {itemId}."); + /// /// Finds the alpha auxiliary image linked to a color image item. /// @@ -2850,7 +2842,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore continue; } - HeifItem candidate = this.FindItemById(link.SourceId)!; + HeifItem candidate = this.FindRequiredItemById(link.SourceId); if (!HeifConstants.IsAlphaAuxiliaryType(candidate.AuxiliaryType)) { continue; @@ -2893,7 +2885,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore List alphaTileIds = new(colorTileIds.Count); foreach (uint colorTileId in colorTileIds) { - HeifItem colorTile = this.FindItemById(colorTileId)!; + HeifItem colorTile = this.FindRequiredItemById(colorTileId); HeifItem? alphaTile = this.FindAlphaItem(colorTile); if (alphaTile is null) { @@ -2935,7 +2927,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore foreach (uint itemId in link.DestinationIds) { - HeifItem tile = this.FindItemById(itemId)!; + HeifItem tile = this.FindRequiredItemById(itemId); if (HeifCompressionFactory.GetDecoder(tile.Type) is null) { // A partially decodable grid cannot yield the requested canvas. Returning no tile lets the @@ -2969,7 +2961,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore return null; } - HeifItem thumbnailItem = this.FindItemById(thumbnailReference.SourceId)!; + HeifItem thumbnailItem = this.FindRequiredItemById(thumbnailReference.SourceId); if (HeifCompressionFactory.GetDecoder(thumbnailItem.Type) is null) { return null; diff --git a/src/ImageSharp/Formats/Heif/HeifItem.cs b/src/ImageSharp/Formats/Heif/HeifItem.cs index 80f095722..eebb831b8 100644 --- a/src/ImageSharp/Formats/Heif/HeifItem.cs +++ b/src/ImageSharp/Formats/Heif/HeifItem.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Heif; /// /// The four-character item type. /// The item identifier used by locations, properties, and references. -internal class HeifItem(Heif4CharCode type, uint id) +internal sealed class HeifItem(Heif4CharCode type, uint id) { /// /// Gets the ID of this Item. diff --git a/src/ImageSharp/Formats/Heif/HeifItemLink.cs b/src/ImageSharp/Formats/Heif/HeifItemLink.cs index 80e6ebdaf..381a8c986 100644 --- a/src/ImageSharp/Formats/Heif/HeifItemLink.cs +++ b/src/ImageSharp/Formats/Heif/HeifItemLink.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Heif; /// /// The four-character reference type. /// The identifier of the item that owns the references. -internal class HeifItemLink(Heif4CharCode type, uint sourceId) +internal sealed class HeifItemLink(Heif4CharCode type, uint sourceId) { /// /// Gets the type of link. diff --git a/src/ImageSharp/Formats/Heif/HeifLocation.cs b/src/ImageSharp/Formats/Heif/HeifLocation.cs index c027fb49a..1485be5dc 100644 --- a/src/ImageSharp/Formats/Heif/HeifLocation.cs +++ b/src/ImageSharp/Formats/Heif/HeifLocation.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Heif; /// The item-location base offset. /// The extent offset relative to the base offset. /// The length of the extent in bytes. -internal class HeifLocation(HeifLocationOffsetOrigin origin, long baseOffset, long offset, long length) +internal sealed class HeifLocation(HeifLocationOffsetOrigin origin, long baseOffset, long offset, long length) { /// /// Gets the origin of the offsets in this location. diff --git a/src/ImageSharp/Formats/Heif/HeifLocationComparer.cs b/src/ImageSharp/Formats/Heif/HeifLocationComparer.cs index c8aaba3af..d20dccb98 100644 --- a/src/ImageSharp/Formats/Heif/HeifLocationComparer.cs +++ b/src/ImageSharp/Formats/Heif/HeifLocationComparer.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif; /// /// Orders item extents by their resolved absolute stream position. /// -internal class HeifLocationComparer : IComparer +internal sealed class HeifLocationComparer : IComparer { /// /// The absolute origin of item-data-relative extents. diff --git a/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ColorConversionBenchmarks.cs b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ColorConversionBenchmarks.cs index 9ef743fe4..668ca4196 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ColorConversionBenchmarks.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ColorConversionBenchmarks.cs @@ -28,17 +28,17 @@ public class Av1ColorConversionBenchmarks /// /// The source RGB image. /// - private Image source = null!; + private Image source; /// /// The destination RGB image. /// - private Image destination = null!; + private Image destination; /// /// The reusable AV1 frame planes. /// - private Av1FrameBuffer frameBuffer = null!; + private Av1FrameBuffer frameBuffer; /// /// Gets or sets the encoded AV1 bit depth. @@ -99,9 +99,9 @@ public class Av1ColorConversionBenchmarks [GlobalCleanup] public void Cleanup() { - this.frameBuffer.Dispose(); - this.destination.Dispose(); - this.source.Dispose(); + this.frameBuffer?.Dispose(); + this.destination?.Dispose(); + this.source?.Dispose(); } /// @@ -111,8 +111,10 @@ public class Av1ColorConversionBenchmarks [Benchmark] public Rgb48 ConvertToRgb() { - Av1YuvConverter.ConvertToRgb(Configuration.Default, this.frameBuffer, this.destination.Frames.RootFrame); - return this.destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(Height - 1)[Width - 1]; + Av1FrameBuffer frameBuffer = this.frameBuffer; + Image destination = this.destination; + Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame); + return destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(Height - 1)[Width - 1]; } /// @@ -122,9 +124,11 @@ public class Av1ColorConversionBenchmarks [Benchmark] public int ConvertFromRgb() { - Av1YuvConverter.ConvertFromRgb(Configuration.Default, this.source.Frames.RootFrame, this.frameBuffer); + Image source = this.source; + Av1FrameBuffer frameBuffer = this.frameBuffer; + Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer); return this.BitDepth == 8 - ? this.frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(Height - 1)[Width - 1] - : this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, Height - 1, 0, 0)[Width - 1]; + ? frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(Height - 1)[Width - 1] + : frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, Height - 1, 0, 0)[Width - 1]; } } diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1BitStreamTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1BitStreamTests.cs index d412f9fdd..b1006faa6 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1BitStreamTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1BitStreamTests.cs @@ -197,8 +197,6 @@ public class Av1BitStreamTests { ulong actual = reader.ReadNonSymmetric(numberOfSymbols); actuals[i] = (uint)actual; - - // Assert.NotEqual(0UL, actual); } Assert.Equal(values, actuals); @@ -311,8 +309,6 @@ public class Av1BitStreamTests { int actual = reader.ReadSignedFromUnsigned(bitCount); actuals[i] = actual; - - // Assert.NotEqual(0, actual); } Assert.Equal(values, actuals); diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ChromaFromLumaTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ChromaFromLumaTests.cs index 5fef02ca0..2d760e965 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ChromaFromLumaTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ChromaFromLumaTests.cs @@ -236,7 +236,7 @@ public class Av1ChromaFromLumaTests Av1ChromaFromLumaContext context = new(colorConfig); context.Store(input, stride, 0, 0, sourceTransform, sourceTransform.ToBlockSize(), 0, 0); context.ComputeParameters(targetTransform); - Assert.Equal(expected, context.Q3Buffer); + Assert.Equal(expected, context.Q3Buffer.ToArray()); short[] highInput = CreateHighBitDepthInput(stride, sourceSize); expected = CreateStoredReference(highInput, stride, sourceSize, subX, subY); @@ -244,7 +244,7 @@ public class Av1ChromaFromLumaTests context = new Av1ChromaFromLumaContext(colorConfig); context.Store(highInput, stride, 0, 0, sourceTransform, sourceTransform.ToBlockSize(), 0, 0); context.ComputeParameters(targetTransform); - Assert.Equal(expected, context.Q3Buffer); + Assert.Equal(expected, context.Q3Buffer.ToArray()); } } } @@ -408,7 +408,7 @@ public class Av1ChromaFromLumaTests { for (int column = 0; column < 32; column++) { - result[(row * 32) + column] = (short)(((row * 7919) + (column * 4051)) % 65521 - 32760); + result[(row * 32) + column] = (short)((((row * 7919) + (column * 4051)) % 65521) - 32760); } } @@ -481,12 +481,12 @@ public class Av1ChromaFromLumaTests /// /// Extracts the active rows from the fixed-stride CfL buffer. /// - private static short[] GetBlock(short[] buffer, int width, int height) + private static short[] GetBlock(ReadOnlySpan buffer, int width, int height) { short[] result = new short[width * height]; for (int y = 0; y < height; y++) { - buffer.AsSpan(y * 32, width).CopyTo(result.AsSpan(y * width, width)); + buffer.Slice(y * 32, width).CopyTo(result.AsSpan(y * width, width)); } return result; diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs index 17032bbc7..8551aacd6 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs @@ -84,7 +84,8 @@ public class Av1CompoundBlockDecoderTests modeInfo.InterpolationFilters.Clear(); modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); - Av1LoopFilterContext loopFilterContext = new(sequenceHeader); + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); using Av1BlockDecoder decoder = new( sequenceHeader, @@ -202,7 +203,8 @@ public class Av1CompoundBlockDecoderTests modeInfo.InterpolationFilters.Fill(Av1InterpolationFilter.Bilinear); modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); - Av1LoopFilterContext loopFilterContext = new(sequenceHeader); + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); using Av1BlockDecoder decoder = new( sequenceHeader, @@ -327,7 +329,8 @@ public class Av1CompoundBlockDecoderTests modeInfo.InterpolationFilters.Clear(); modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); - Av1LoopFilterContext loopFilterContext = new(sequenceHeader); + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); using Av1BlockDecoder decoder = new( sequenceHeader, @@ -417,7 +420,8 @@ public class Av1CompoundBlockDecoderTests modeInfo.InterpolationFilters.Clear(); modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); - Av1LoopFilterContext loopFilterContext = new(sequenceHeader); + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); using Av1BlockDecoder decoder = new( sequenceHeader, @@ -499,7 +503,8 @@ public class Av1CompoundBlockDecoderTests current.SetTransformUnitCount(Av1PlaneType.Y, 1); frameInfo.UpdateModeInfo(current, superblockInfo); - Av1LoopFilterContext loopFilterContext = new(sequenceHeader); + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); using Av1BlockDecoder decoder = new( sequenceHeader, @@ -594,7 +599,8 @@ public class Av1CompoundBlockDecoderTests current.SetTransformUnitCount(Av1PlaneType.Uv, 1); frameInfo.UpdateModeInfo(current, superblockInfo); - Av1LoopFilterContext loopFilterContext = new(sequenceHeader); + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); using Av1BlockDecoder decoder = new( sequenceHeader, @@ -739,7 +745,7 @@ public class Av1CompoundBlockDecoderTests for (int referenceIndex = 0; referenceIndex < 2; referenceIndex++) { - Av1FrameBuffer reference = referenceFrames.Resolve(referenceIndex)!.FrameBuffer; + Av1FrameBuffer reference = referenceFrames.ResolveRequired(referenceIndex).FrameBuffer; Span intermediate = referenceIndex == 0 ? firstIntermediate : secondIntermediate; int sourceIndex; if (bitDepth == Av1BitDepth.EightBit) @@ -849,7 +855,8 @@ public class Av1CompoundBlockDecoderTests using Av1FrameInfo frameInfo = new(sequenceHeader); Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); - Av1LoopFilterContext loopFilterContext = new(sequenceHeader); + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); using Av1BlockDecoder decoder = new( sequenceHeader, @@ -1049,7 +1056,7 @@ public class Av1CompoundBlockDecoderTests short[] predictionScratch = new short[128 * (blockSize + 8)]; for (int referenceIndex = 0; referenceIndex < 2; referenceIndex++) { - Av1FrameBuffer reference = referenceFrames.Resolve(referenceIndex)!.FrameBuffer; + Av1FrameBuffer reference = referenceFrames.ResolveRequired(referenceIndex).FrameBuffer; Span source = reference.GetPaddedPlaneSpan16( Av1Plane.Y, 0, @@ -1243,7 +1250,8 @@ public class Av1CompoundBlockDecoderTests using Av1FrameInfo frameInfo = new(sequenceHeader); Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); - Av1LoopFilterContext loopFilterContext = new(sequenceHeader); + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); using Av1BlockDecoder decoder = new( sequenceHeader, @@ -1280,14 +1288,14 @@ public class Av1CompoundBlockDecoderTests const int blockOrigin = 8; const int blockSize = 8; int bitDepthValue = bitDepth.GetBitCount(); - int intermediateRange = bitDepthValue + Av1InterPredictor.FilterBits - Av1InterPredictor.Round0Bits + 2; - int round0 = Av1InterPredictor.Round0Bits + Math.Max(intermediateRange - 16, 0); + int intermediateRange = bitDepthValue + Av1TranslationalInterPredictor.FilterBits - Av1TranslationalInterPredictor.Round0Bits + 2; + int round0 = Av1TranslationalInterPredictor.Round0Bits + Math.Max(intermediateRange - 16, 0); int compoundRoundBits = - (2 * Av1InterPredictor.FilterBits) - round0 - Av1CompoundInterPredictor.CompoundRound1Bits; + (2 * Av1TranslationalInterPredictor.FilterBits) - round0 - Av1CompoundInterPredictor.CompoundRound1Bits; int compoundOffsetBits = bitDepthValue + - (2 * Av1InterPredictor.FilterBits) - + (2 * Av1TranslationalInterPredictor.FilterBits) - round0 - Av1CompoundInterPredictor.CompoundRound1Bits; @@ -1322,8 +1330,8 @@ public class Av1CompoundBlockDecoderTests CreatePatternReferenceFrame(sequenceHeader, CreateFrameHeader(frameSize), sampleOffset: 40), showFrame: false)); - Av1FrameBuffer firstReference = referenceFrames.Resolve(0)!.FrameBuffer; - Av1FrameBuffer secondReference = referenceFrames.Resolve(1)!.FrameBuffer; + Av1FrameBuffer firstReference = referenceFrames.ResolveRequired(0).FrameBuffer; + Av1FrameBuffer secondReference = referenceFrames.ResolveRequired(1).FrameBuffer; ushort[] firstHighBitDepthPrediction = new ushort[blockSize * blockSize]; ushort[] secondHighBitDepthPrediction = new ushort[blockSize * blockSize]; short[] firstScratch = new short[Av1WarpedInterPredictor.WarpedScratchLength]; @@ -1451,7 +1459,8 @@ public class Av1CompoundBlockDecoderTests modeInfo.InterpolationFilters.Clear(); modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); - Av1LoopFilterContext loopFilterContext = new(sequenceHeader); + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); using Av1BlockDecoder decoder = new( sequenceHeader, diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1DeblockingFilterTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1DeblockingFilterTests.cs index 24b728037..24080444e 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1DeblockingFilterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1DeblockingFilterTests.cs @@ -170,9 +170,10 @@ public class Av1DeblockingFilterTests rightModeInfo.ReferenceFrames[0] = referenceFrame; frameInfo.UpdateModeInfo(leftModeInfo, superblock); frameInfo.UpdateModeInfo(rightModeInfo, superblock); - superblock.BlockCount = 2; - Av1LoopFilterContext loopFilterContext = new(sequenceHeader); + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); + loopFilterContext.SetTransformSize(Av1Plane.Y, Point.Empty, Av1TransformSize.Size8x8); loopFilterContext.SetTransformSize(Av1Plane.Y, new Point(2, 0), Av1TransformSize.Size8x8); loopFilterContext.SetTransformSize(Av1Plane.Y, new Point(4, 0), Av1TransformSize.Size8x8); diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ForwardTransformTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ForwardTransformTests.cs index 6fa3c6d64..f5c9a7447 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ForwardTransformTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ForwardTransformTests.cs @@ -567,7 +567,7 @@ public class Av1ForwardTransformTests /// The number of input samples between rows. /// The destination reference coefficients. /// The resolved transform functions, shifts, and axis orientation. - private static void DispatchReferenceColumn(Span input, int stride, Span output, ref Av1Transform2dFlipConfiguration config) + private static void DispatchReferenceColumn(ReadOnlySpan input, int stride, Span output, ref Av1Transform2dFlipConfiguration config) { switch (config.TransformFunctionTypeColumn) { @@ -618,7 +618,7 @@ public class Av1ForwardTransformTests /// The number of input samples between rows. /// The destination reference coefficients. /// The resolved transform functions, shifts, and axis orientation. - private static void DispatchReferenceRow(Span input, int stride, Span output, ref Av1Transform2dFlipConfiguration config) + private static void DispatchReferenceRow(ReadOnlySpan input, int stride, Span output, ref Av1Transform2dFlipConfiguration config) where TColumnOperator : struct, Av1ForwardTransformer.IAv1ForwardTransform1dOperator { switch (config.TransformFunctionTypeRow) @@ -672,7 +672,7 @@ public class Av1ForwardTransformTests /// The destination reference coefficients. /// The resolved transform functions, shifts, and axis orientation. private static void TransformReference( - Span input, + ReadOnlySpan input, int stride, Span output, ref Av1Transform2dFlipConfiguration config) @@ -752,7 +752,7 @@ public class Av1ForwardTransformTests 1 => short.MinValue, 2 => 255, 3 => -255, - _ => ((index * 73) + (lane * 151)) % 511 - 255, + _ => (((index * 73) + (lane * 151)) % 511) - 255, }); /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1FrameBufferTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1FrameBufferTests.cs index 1cb4de99f..a3e0c5345 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1FrameBufferTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1FrameBufferTests.cs @@ -5,6 +5,8 @@ using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; +using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Tests.Memory; @@ -43,7 +45,7 @@ public class Av1FrameBufferTests Av1ColorFormat.Yuv400, false); - MemoryGroup memoryGroup = frameBuffer.BufferY!.FastMemoryGroup; + MemoryGroup memoryGroup = frameBuffer.GetPlaneBuffer(Av1Plane.Y).FastMemoryGroup; Assert.Equal(1, memoryGroup.Count); Assert.True(memoryGroup.TotalLength > allocator.BufferCapacityInBytes); } @@ -105,17 +107,24 @@ public class Av1FrameBufferTests Assert.Equal(3, allocator.AllocationAttemptCount); Assert.Equal(2, allocator.AllocationLog.Count); Assert.Equal(2, allocator.ReturnLog.Count); - Assert.Equal(allocator.AllocationLog[0].HashCodeOfBuffer, allocator.ReturnLog[0].HashCodeOfBuffer); - Assert.Equal(allocator.AllocationLog[1].HashCodeOfBuffer, allocator.ReturnLog[1].HashCodeOfBuffer); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); } /// - /// Verifies that a block-decoder workspace failure releases every workspace rented earlier in construction. + /// Verifies that each later block-decoder workspace failure releases every workspace rented earlier. /// - [Fact] - public void BlockDecoderConstructorFailureReleasesEarlierWorkspaces() + [Theory] + [InlineData(3, 1)] + [InlineData(4, 2)] + public void BlockDecoderConstructorFailureReleasesEarlierWorkspaces( + int failureAllocationNumber, + int successfulWorkspaceCount) { - FailingTestMemoryAllocator allocator = new(failureAllocationNumber: 3); + FailingTestMemoryAllocator allocator = new(failureAllocationNumber); Configuration configuration = Configuration.Default.Clone(); configuration.MemoryAllocator = allocator; ObuSequenceHeader sequenceHeader = new() @@ -131,24 +140,172 @@ public class Av1FrameBufferTests }; using Av1FrameBuffer frameBuffer = new(configuration, sequenceHeader, Av1ColorFormat.Yuv400, false); - ObuFrameHeader frameHeader = new(); - Av1LoopFilterContext loopFilterContext = new(sequenceHeader); + ObuFrameHeader frameHeader = new() + { + ModeInfoColumnCount = 16, + ModeInfoRowCount = 16 + }; + + using Av1LoopFilterContext loopFilterContext = + new(Configuration.Default.MemoryAllocator, sequenceHeader, frameHeader); + Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); + using Av1ReferenceFrameStore referenceFrames = new(); - // The frame's luma plane is allocation attempt one. Resetting only the logs preserves that counter, so the - // inverse-quantization workspace succeeds on attempt two and the transform workspace fails on attempt three. + // The frame's luma plane is allocation attempt one. Resetting only the logs preserves that counter while + // isolating the block-decoder owners that must be returned when a later workspace rent fails. allocator.EnableNonThreadSafeLogging(); Assert.Throws( - () => new Av1BlockDecoder(sequenceHeader, frameHeader, frameBuffer, loopFilterContext, inverseQuantizer)); + () => new Av1BlockDecoder( + sequenceHeader, + frameHeader, + frameBuffer, + loopFilterContext, + inverseQuantizer, + referenceFrames)); + + Assert.Equal(failureAllocationNumber, allocator.AllocationAttemptCount); + Assert.Equal(successfulWorkspaceCount, allocator.AllocationLog.Count); + Assert.Equal(successfulWorkspaceCount, allocator.ReturnLog.Count); + foreach (TestMemoryAllocator.AllocationRequest allocation in allocator.AllocationLog) + { + Assert.Contains( + allocator.ReturnLog, + returned => returned.HashCodeOfBuffer == allocation.HashCodeOfBuffer); + } + } + + /// + /// Verifies that failure to allocate the active chroma transform map releases the preceding luma map. + /// + [Fact] + public void LoopFilterContextAllocationFailureReleasesLumaMap() + { + FailingTestMemoryAllocator allocator = new(failureAllocationNumber: 2); + ObuSequenceHeader sequenceHeader = new() + { + MaxFrameWidth = 64, + MaxFrameHeight = 64, + Use128x128Superblock = false, + ColorConfig = new ObuColorConfig + { + IsMonochrome = false, + SubSamplingX = true, + SubSamplingY = true, + BitDepth = Av1BitDepth.EightBit + } + }; + + ObuFrameHeader frameHeader = new() + { + ModeInfoColumnCount = 16, + ModeInfoRowCount = 16 + }; + + Assert.Throws( + () => new Av1LoopFilterContext(allocator, sequenceHeader, frameHeader)); TestMemoryAllocator.AllocationRequest allocation = Assert.Single(allocator.AllocationLog); TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); - Assert.Equal(3, allocator.AllocationAttemptCount); + Assert.Equal(2, allocator.AllocationAttemptCount); Assert.Equal(allocation.HashCodeOfBuffer, returned.HashCodeOfBuffer); } + /// + /// Verifies that active-superblock coefficient scratch uses one configured allocator lease and omits unused + /// chroma storage for a monochrome frame. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void FrameInfoCoefficientScratchUsesConfiguredAllocator() + { + TestMemoryAllocator allocator = new(); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + ObuSequenceHeader sequenceHeader = new() + { + MaxFrameWidth = 64, + MaxFrameHeight = 64, + Use128x128Superblock = false, + ColorConfig = new ObuColorConfig + { + IsMonochrome = true, + SubSamplingX = true, + SubSamplingY = true, + BitDepth = Av1BitDepth.EightBit + } + }; + + ObuFrameHeader frameHeader = new() + { + FrameSize = new ObuFrameSize + { + FrameWidth = 64, + FrameHeight = 64 + } + }; + + Av1FrameInfo frameInfo = new(configuration, sequenceHeader, frameHeader); + int expectedCoefficientCount = 16 * 16 * Av1FrameInfo.CoefficientCountPerModeInfo; + TestMemoryAllocator.AllocationRequest coefficientScratch = Assert.Single( + allocator.AllocationLog, + request => request.ElementType == typeof(int) && request.Length == expectedCoefficientCount); + + Assert.Equal(expectedCoefficientCount, frameInfo.GetCoefficientsY().Length); + Assert.Equal(0, frameInfo.GetCoefficientsU().Length); + Assert.Equal(0, frameInfo.GetCoefficientsV().Length); + + frameInfo.Dispose(); + + Assert.Single( + allocator.ReturnLog, + returned => returned.HashCodeOfBuffer == coefficientScratch.HashCodeOfBuffer); + } + + /// + /// Verifies that a later frame-state allocation failure returns the coefficient scratch rented first. + /// + [Fact] + public void FrameInfoConstructorFailureReleasesCoefficientScratch() + { + FailingTestMemoryAllocator allocator = new(failureAllocationNumber: 2); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + ObuSequenceHeader sequenceHeader = new() + { + MaxFrameWidth = 64, + MaxFrameHeight = 64, + Use128x128Superblock = false, + ColorConfig = new ObuColorConfig + { + IsMonochrome = true, + SubSamplingX = true, + SubSamplingY = true, + BitDepth = Av1BitDepth.EightBit + } + }; + + ObuFrameHeader frameHeader = new() + { + FrameSize = new ObuFrameSize + { + FrameWidth = 64, + FrameHeight = 64 + } + }; + + Assert.Throws(() => new Av1FrameInfo(configuration, sequenceHeader, frameHeader)); + + TestMemoryAllocator.AllocationRequest coefficientScratch = Assert.Single(allocator.AllocationLog); + TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); + Assert.Equal(typeof(int), coefficientScratch.ElementType); + Assert.Equal(2, allocator.AllocationAttemptCount); + Assert.Equal(coefficientScratch.HashCodeOfBuffer, returned.HashCodeOfBuffer); + } + /// /// Provides tracked plane owners until the configured allocation attempt fails. /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterFrameModeInfoTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterFrameModeInfoTests.cs index ce60bddee..316c9ebe9 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterFrameModeInfoTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterFrameModeInfoTests.cs @@ -121,7 +121,7 @@ public class Av1InterFrameModeInfoTests // translational global model still requires interpolation, leaving only the filter branch under test. ObuSegmentationParameters segmentationParameters = frameHeader.SegmentationParameters; segmentationParameters.Enabled = true; - segmentationParameters.FeatureEnabled[0, (int)ObuSegmentationLevelFeature.GlobalMotionVector] = true; + segmentationParameters.SetFeatureEnabled(0, (int)ObuSegmentationLevelFeature.GlobalMotionVector, true); frameHeader.GetGlobalMotionParameters()[0].Type = Av1GlobalMotionType.Translation; using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader); @@ -158,7 +158,7 @@ public class Av1InterFrameModeInfoTests frameHeader.InterpolationFilter = Av1InterpolationFilter.Switchable; ObuSegmentationParameters segmentationParameters = frameHeader.SegmentationParameters; segmentationParameters.Enabled = true; - segmentationParameters.FeatureEnabled[0, (int)ObuSegmentationLevelFeature.GlobalMotionVector] = true; + segmentationParameters.SetFeatureEnabled(0, (int)ObuSegmentationLevelFeature.GlobalMotionVector, true); using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader); Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty); @@ -424,6 +424,14 @@ public class Av1InterFrameModeInfoTests ModeInfoRowCount = 16, CodedLossless = true, AllowScreenContentTools = false, + FrameSize = new ObuFrameSize + { + FrameWidth = 64, + FrameHeight = 64, + SuperResolutionUpscaledWidth = 64, + RenderWidth = 64, + RenderHeight = 64, + }, }; frameHeader.TilesInfo.TileColumnStartModeInfo[1] = frameHeader.ModeInfoColumnCount; diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyTests.cs index d0af7d9b3..f20bc25d4 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyTests.cs @@ -21,7 +21,7 @@ public class Av1IntraBlockCopyTests public void FindReferenceUsesFirstRowFallback() { ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); - Av1FrameInfo frameInfo = new(sequenceHeader); + using Av1FrameInfo frameInfo = new(sequenceHeader); Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(5, 0)); Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, Point.Empty); Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) @@ -51,11 +51,10 @@ public class Av1IntraBlockCopyTests public void FindReferenceUsesPreviousSuperblockRowFallback() { ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); - Av1FrameInfo frameInfo = new(sequenceHeader); + using Av1FrameInfo frameInfo = new(sequenceHeader); Av1SuperblockInfo aboveSuperblock = frameInfo.GetSuperblock(new Point(5, 0)); Av1BlockModeInfo aboveModeInfo = new(Av1BlockSize.Block64x64, Point.Empty); frameInfo.UpdateModeInfo(aboveModeInfo, aboveSuperblock); - aboveSuperblock.BlockCount++; Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(5, 1)); Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, Point.Empty); @@ -87,7 +86,7 @@ public class Av1IntraBlockCopyTests public void IsValidEnforcesIntraBlockCopySourceRestrictions() { ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); - Av1FrameInfo frameInfo = new(sequenceHeader); + using Av1FrameInfo frameInfo = new(sequenceHeader); Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(8, 2)); Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, Point.Empty); Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) @@ -131,17 +130,20 @@ public class Av1IntraBlockCopyTests /// private static Av1TileInfo CreateTileInfo() { + ObuTileGroupHeader tilesInfo = new() + { + TileColumnCount = 1, + TileRowCount = 1, + }; + + tilesInfo.TileColumnStartModeInfo[1] = 160; + tilesInfo.TileRowStartModeInfo[1] = 64; + ObuFrameHeader frameHeader = new() { ModeInfoColumnCount = 160, ModeInfoRowCount = 64, - TilesInfo = new ObuTileGroupHeader - { - TileColumnCount = 1, - TileRowCount = 1, - TileColumnStartModeInfo = [0, 160], - TileRowStartModeInfo = [0, 64], - }, + TilesInfo = tilesInfo, }; return new Av1TileInfo(0, 0, frameHeader); diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionModeInfoTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionModeInfoTests.cs index 3a7be2fe1..984b3f6dd 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionModeInfoTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionModeInfoTests.cs @@ -106,7 +106,6 @@ public class Av1MotionModeInfoTests aboveModeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.None; aboveModeInfo.InterpolationFilters.Clear(); tileReader.FrameInfo.UpdateModeInfo(aboveModeInfo, superblockInfo); - superblockInfo.BlockCount++; Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, new Point(0, 2)); Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None) @@ -226,7 +225,17 @@ public class Av1MotionModeInfoTests /// /// The initialized frame header. private static ObuFrameHeader CreateFrameHeader() - => new() + { + ObuTileGroupHeader tilesInfo = new() + { + TileColumnCount = 1, + TileRowCount = 1, + }; + + tilesInfo.TileColumnStartModeInfo[1] = 16; + tilesInfo.TileRowStartModeInfo[1] = 16; + + return new() { FrameType = ObuFrameType.InterFrame, ModeInfoColumnCount = 16, @@ -242,14 +251,9 @@ public class Av1MotionModeInfoTests RenderWidth = 64, RenderHeight = 64, }, - TilesInfo = new ObuTileGroupHeader - { - TileColumnCount = 1, - TileRowCount = 1, - TileColumnStartModeInfo = [0, 16], - TileRowStartModeInfo = [0, 16], - }, + TilesInfo = tilesInfo, }; + } /// /// Forces segment zero to a translational global-motion mode that omits reference and inter-mode symbols but still carries interpolation. @@ -259,7 +263,7 @@ public class Av1MotionModeInfoTests { ObuSegmentationParameters segmentationParameters = frameHeader.SegmentationParameters; segmentationParameters.Enabled = true; - segmentationParameters.FeatureEnabled[0, (int)ObuSegmentationLevelFeature.GlobalMotionVector] = true; + segmentationParameters.SetFeatureEnabled(0, (int)ObuSegmentationLevelFeature.GlobalMotionVector, true); frameHeader.GetGlobalMotionParameters()[0].Type = Av1GlobalMotionType.Translation; frameHeader.GetReferenceFrameIndices()[0] = 0; } diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVariationCandidatesTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVariationCandidatesTests.cs index 6cbe1ce40..bae07daf7 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVariationCandidatesTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVariationCandidatesTests.cs @@ -23,7 +23,7 @@ public class Av1MotionVariationCandidatesTests { ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); ObuFrameHeader frameHeader = CreateFrameHeader(); - Av1FrameInfo frameInfo = new(sequenceHeader); + using Av1FrameInfo frameInfo = new(sequenceHeader); for (int offset = 0; offset < 8; offset += 2) { @@ -79,7 +79,7 @@ public class Av1MotionVariationCandidatesTests { ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); ObuFrameHeader frameHeader = CreateFrameHeader(); - Av1FrameInfo horizontalFrameInfo = new(sequenceHeader); + using Av1FrameInfo horizontalFrameInfo = new(sequenceHeader); AddModeInfo( horizontalFrameInfo, sequenceHeader, @@ -114,7 +114,7 @@ public class Av1MotionVariationCandidatesTests frameHeader, Av1ReferenceFrameType.Last); - Av1FrameInfo verticalFrameInfo = new(sequenceHeader); + using Av1FrameInfo verticalFrameInfo = new(sequenceHeader); AddModeInfo( verticalFrameInfo, sequenceHeader, @@ -161,7 +161,7 @@ public class Av1MotionVariationCandidatesTests { ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); ObuFrameHeader frameHeader = CreateFrameHeader(); - Av1FrameInfo frameInfo = new(sequenceHeader); + using Av1FrameInfo frameInfo = new(sequenceHeader); // The first candidate has the wrong primary reference and the second is compound. Ten following candidates // are eligible, so the retained range must begin at offset two and stop after eight samples at offset nine. @@ -220,7 +220,7 @@ public class Av1MotionVariationCandidatesTests { ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); ObuFrameHeader frameHeader = CreateFrameHeader(); - Av1FrameInfo frameInfo = new(sequenceHeader); + using Av1FrameInfo frameInfo = new(sequenceHeader); AddModeInfo(frameInfo, sequenceHeader, new Point(4, 2), Av1BlockSize.Block8x8, Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None, default); AddModeInfo(frameInfo, sequenceHeader, new Point(2, 4), Av1BlockSize.Block8x8, Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None, default); AddModeInfo(frameInfo, sequenceHeader, new Point(2, 2), Av1BlockSize.Block8x8, Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None, default); @@ -258,7 +258,7 @@ public class Av1MotionVariationCandidatesTests { ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); ObuFrameHeader frameHeader = CreateFrameHeader(); - Av1FrameInfo frameInfo = new(sequenceHeader); + using Av1FrameInfo frameInfo = new(sequenceHeader); // The aligned 16x8 above block covers the top-right position. The 8x16 left block begins two mode-info rows // above the current block and therefore covers its top-left position. @@ -307,19 +307,24 @@ public class Av1MotionVariationCandidatesTests /// /// The initialized frame header. private static ObuFrameHeader CreateFrameHeader() - => new() + { + ObuTileGroupHeader tilesInfo = new() + { + TileColumnCount = 1, + TileRowCount = 1, + }; + + tilesInfo.TileColumnStartModeInfo[1] = 32; + tilesInfo.TileRowStartModeInfo[1] = 32; + + return new() { FrameType = ObuFrameType.InterFrame, ModeInfoColumnCount = 32, ModeInfoRowCount = 32, - TilesInfo = new ObuTileGroupHeader - { - TileColumnCount = 1, - TileRowCount = 1, - TileColumnStartModeInfo = [0, 32], - TileRowStartModeInfo = [0, 32], - }, + TilesInfo = tilesInfo, }; + } /// /// Creates one current partition at a frame-relative mode-information position. @@ -387,6 +392,5 @@ public class Av1MotionVariationCandidatesTests modeInfo.ReferenceFrames[1] = secondaryReference; modeInfo.MotionVectors[0] = motionVector; frameInfo.UpdateModeInfo(modeInfo, superblockInfo); - superblockInfo.BlockCount++; } } diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs index 952349abd..eb6e368b0 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; @@ -27,12 +28,12 @@ public class Av1PredictorTests HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; /// - /// Cardinal, base, and adjusted angles covering every directional projection zone. + /// Gets the cardinal, base, and adjusted angles covering every directional projection zone. /// private static ReadOnlySpan DirectionalAngles => [36, 45, 54, 67, 90, 104, 113, 126, 135, 148, 157, 166, 180, 194, 203, 212]; /// - /// The complete set of AV1 filter-intra coefficient modes. + /// Gets the complete set of AV1 filter-intra coefficient modes. /// private static ReadOnlySpan FilterIntraModes => [ @@ -120,6 +121,42 @@ public class Av1PredictorTests public void EdgeFilteringMatchesReference() => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateEdgeFiltering, HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic); + /// + /// Verifies the traversal-order bits that distinguish current libaom's mixed-vertical square tables. + /// + [Fact] + public void MixedVerticalAvailabilityUsesDedicatedSquareTables() + { + Assert.True(Av1BottomRightTopLeftConstants.HasTopRight(Av1PartitionType.Split, Av1BlockSize.Block8x8, 16)); + Assert.False(Av1BottomRightTopLeftConstants.HasTopRight(Av1PartitionType.VerticalA, Av1BlockSize.Block8x8, 16)); + Assert.False(Av1BottomRightTopLeftConstants.HasTopRight(Av1PartitionType.VerticalB, Av1BlockSize.Block8x8, 16)); + + Assert.False(Av1BottomRightTopLeftConstants.HasBottomLeft(Av1PartitionType.Split, Av1BlockSize.Block8x8, 1)); + Assert.True(Av1BottomRightTopLeftConstants.HasBottomLeft(Av1PartitionType.VerticalA, Av1BlockSize.Block8x8, 1)); + Assert.True(Av1BottomRightTopLeftConstants.HasBottomLeft(Av1PartitionType.VerticalB, Av1BlockSize.Block8x8, 1)); + } + + /// + /// Verifies that mixed-vertical rectangles use current libaom's ordinary rectangle tables. + /// + [Theory] + [InlineData((int)Av1BlockSize.Block4x8)] + [InlineData((int)Av1BlockSize.Block8x16)] + [InlineData((int)Av1BlockSize.Block16x32)] + [InlineData((int)Av1BlockSize.Block32x64)] + [InlineData((int)Av1BlockSize.Block64x128)] + public void MixedVerticalAvailabilityReusesVerticalRectangleTables(int blockSizeValue) + { + Av1BlockSize blockSize = (Av1BlockSize)blockSizeValue; + bool expectedTopRight = Av1BottomRightTopLeftConstants.HasTopRight(Av1PartitionType.Split, blockSize, 0); + bool expectedBottomLeft = Av1BottomRightTopLeftConstants.HasBottomLeft(Av1PartitionType.Split, blockSize, 0); + + Assert.Equal(expectedTopRight, Av1BottomRightTopLeftConstants.HasTopRight(Av1PartitionType.VerticalA, blockSize, 0)); + Assert.Equal(expectedTopRight, Av1BottomRightTopLeftConstants.HasTopRight(Av1PartitionType.VerticalB, blockSize, 0)); + Assert.Equal(expectedBottomLeft, Av1BottomRightTopLeftConstants.HasBottomLeft(Av1PartitionType.VerticalA, blockSize, 0)); + Assert.Equal(expectedBottomLeft, Av1BottomRightTopLeftConstants.HasBottomLeft(Av1PartitionType.VerticalB, blockSize, 0)); + } + /// /// Verifies all four DC neighbor-availability combinations at every AV1 transform size. /// @@ -192,7 +229,7 @@ public class Av1PredictorTests /// The prediction mode to verify. private static void ValidateNonDirectionalPredictor(Av1PredictionMode mode) { - Av1IntraPredictorBase predictor = Av1IntraPredictorBase.GetPredictor(mode); + Av1NonDirectionalIntraPredictorBase predictor = Av1NonDirectionalIntraPredictorBase.GetPredictor(mode); for (int sizeIndex = 0; sizeIndex < (int)Av1TransformSize.AllSizes; sizeIndex++) { Av1TransformSize transformSize = (Av1TransformSize)sizeIndex; @@ -225,7 +262,7 @@ public class Av1PredictorTests /// /// The prediction mode being verified. /// The closed operator-driven predictor. - private static void ValidateKnownNonDirectionalVector(Av1PredictionMode mode, Av1IntraPredictorBase predictor) + private static void ValidateKnownNonDirectionalVector(Av1PredictionMode mode, Av1NonDirectionalIntraPredictorBase predictor) { byte[] aboveStorage; byte[] left; diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs index 7833392cc..19ff55367 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs @@ -748,6 +748,8 @@ public class Av1ReconstructionConformanceTests ValidateProgressiveSingleReferenceFixture(configuration); + Assert.Contains(allocator.AllocationLog, allocation => allocation.ElementType == typeof(Av1BlockModeInfo)); + Assert.Contains(allocator.AllocationLog, allocation => allocation.ElementType == typeof(Av1TransformInfo)); Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); Assert.All( allocator.AllocationLog, @@ -991,35 +993,39 @@ public class Av1ReconstructionConformanceTests using ImageFrame frame = decodedFrame; - ObuSequenceHeader sequenceHeader = Assert.IsType(decoder.SequenceHeader); _ = Assert.IsType(decoder.FrameHeader); Av1FrameBuffer frameBuffer = Assert.IsType>(decoder.FrameBuffer); - Av1FrameInfo frameInfo = Assert.IsType(decoder.FrameInfo); + Av1FrameInfo frameInfo = decoder.FrameInfo; // Inter prediction addresses padding with one base span and a logical row stride. The frame owner must // preserve that contract even when the configured allocator would ordinarily split a large buffer. - Assert.Equal(1, frameBuffer.BufferY!.FastMemoryGroup.Count); - Assert.Equal(1, frameBuffer.BufferCb!.FastMemoryGroup.Count); - Assert.Equal(1, frameBuffer.BufferCr!.FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.Y).FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.U).FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.V).FastMemoryGroup.Count); - int superblockSizeLog2 = sequenceHeader.SuperblockSizeLog2; - int superblockColumnCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, superblockSizeLog2) >> superblockSizeLog2; - int superblockRowCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameHeight, superblockSizeLog2) >> superblockSizeLog2; - - for (int superblockRow = 0; superblockRow < superblockRowCount; superblockRow++) + // A pure show_existing_frame payload presents a retained reference without decoding new block syntax. + if (frameInfo is not null) { - for (int superblockColumn = 0; superblockColumn < superblockColumnCount; superblockColumn++) + ObuSequenceHeader sequenceHeader = Assert.IsType(decoder.SequenceHeader); + int superblockSizeLog2 = sequenceHeader.SuperblockSizeLog2; + int superblockColumnCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, superblockSizeLog2) >> superblockSizeLog2; + int superblockRowCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameHeight, superblockSizeLog2) >> superblockSizeLog2; + + for (int superblockRow = 0; superblockRow < superblockRowCount; superblockRow++) { - Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(superblockColumn, superblockRow)); - foreach (Av1BlockModeInfo modeInfo in superblockInfo.GetModeInfos()) + for (int superblockColumn = 0; superblockColumn < superblockColumnCount; superblockColumn++) { - if (modeInfo.ReferenceFrames[1] <= Av1ReferenceFrameType.Intra) + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(superblockColumn, superblockRow)); + foreach (Av1BlockModeInfo modeInfo in superblockInfo.GetModeInfos()) { - continue; - } + if (modeInfo.ReferenceFrames[1] <= Av1ReferenceFrameType.Intra) + { + continue; + } - Assert.Equal(Av1CompoundType.Average, modeInfo.CompoundType); - compoundBlockCount++; + Assert.Equal(Av1CompoundType.Average, modeInfo.CompoundType); + compoundBlockCount++; + } } } } @@ -1252,6 +1258,7 @@ public class Av1ReconstructionConformanceTests ObuSequenceHeader sequenceHeader = Assert.IsType(decoder.SequenceHeader); Av1FrameInfo frameInfo = Assert.IsType(decoder.FrameInfo); + transformTypeCoverage |= frameInfo.LumaTransformTypeCoverage; int superblockColumnCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, sequenceHeader.SuperblockSizeLog2) >> sequenceHeader.SuperblockSizeLog2; int superblockRowCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameHeight, sequenceHeader.SuperblockSizeLog2) @@ -1268,14 +1275,6 @@ public class Av1ReconstructionConformanceTests { intraModeCoverage |= 1 << ((int)modeInfo.YMode - (int)Av1PredictionMode.IntraModeStart); } - - int firstTransformLocation = modeInfo.GetFirstTransformLocation(Av1Plane.Y); - int transformUnitCount = modeInfo.GetTransformUnitCount(Av1Plane.Y); - foreach (Av1TransformInfo transformInfo in - superblockInfo.GetTransformInfoY().Slice(firstTransformLocation, transformUnitCount)) - { - transformTypeCoverage |= 1 << (int)transformInfo.Type; - } } } } @@ -2224,8 +2223,14 @@ public class Av1ReconstructionConformanceTests nativeOffset += nativeFrameLength; + Av1FrameInfo frameInfo = decoder.FrameInfo; + if (frameInfo is null) + { + // show_existing_frame contributes no new mode or interpolation-filter syntax. + continue; + } + ObuSequenceHeader sequenceHeader = Assert.IsType(decoder.SequenceHeader); - Av1FrameInfo frameInfo = Assert.IsType(decoder.FrameInfo); int superblockColumnCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, sequenceHeader.SuperblockSizeLog2) >> sequenceHeader.SuperblockSizeLog2; int superblockRowCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameHeight, sequenceHeader.SuperblockSizeLog2) @@ -2436,9 +2441,9 @@ public class Av1ReconstructionConformanceTests coverage |= GetInterPredictionCoverage(decoder); // Every inter-prediction branch retains the same row-addressed plane contract under constrained allocators. - Assert.Equal(1, frameBuffer.BufferY!.FastMemoryGroup.Count); - Assert.Equal(1, frameBuffer.BufferCb!.FastMemoryGroup.Count); - Assert.Equal(1, frameBuffer.BufferCr!.FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.Y).FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.U).FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.V).FastMemoryGroup.Count); if (decodedVisibleFrameCount == visibleFrameCount - 1) { @@ -2455,82 +2460,10 @@ public class Av1ReconstructionConformanceTests } /// - /// Collects the compound, inter-intra, OBMC, and warped modes retained in one decoded frame. + /// Collects the compound, inter-intra, OBMC, and warped modes completed in one bounded payload. /// - private static int GetInterPredictionCoverage(Av1Decoder decoder) - { - ObuSequenceHeader sequenceHeader = Assert.IsType(decoder.SequenceHeader); - ObuFrameHeader frameHeader = Assert.IsType(decoder.FrameHeader); - Av1FrameInfo frameInfo = Assert.IsType(decoder.FrameInfo); - int superblockSizeLog2 = sequenceHeader.SuperblockSizeLog2; - int superblockColumnCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, superblockSizeLog2) >> superblockSizeLog2; - int superblockRowCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameHeight, superblockSizeLog2) >> superblockSizeLog2; - int coverage = 0; - for (int superblockRow = 0; superblockRow < superblockRowCount; superblockRow++) - { - for (int superblockColumn = 0; superblockColumn < superblockColumnCount; superblockColumn++) - { - Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(superblockColumn, superblockRow)); - foreach (Av1BlockModeInfo modeInfo in superblockInfo.GetModeInfos()) - { - if (modeInfo.MotionMode == Av1MotionMode.Obmc) - { - coverage |= ObmcCoverage; - } - - if (modeInfo.MotionMode == Av1MotionMode.Warped) - { - coverage |= LocalWarpCoverage; - } - - 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; - for (int referenceIndex = 0; referenceIndex < referenceCount; referenceIndex++) - { - int canonicalReferenceIndex = - (int)modeInfo.ReferenceFrames[referenceIndex] - (int)Av1ReferenceFrameType.Last; - - Av1GlobalMotionParameters globalMotionParameters = - frameHeader.GetGlobalMotionParameters()[canonicalReferenceIndex]; - - if (globalMotionParameters.Type > Av1GlobalMotionType.Translation && - !globalMotionParameters.IsInvalid) - { - coverage |= GlobalWarpCoverage; - } - } - } - - if (modeInfo.ReferenceFrames[1] == Av1ReferenceFrameType.Intra) - { - coverage |= modeInfo.UseInterIntraWedge ? WedgeInterIntraCoverage : SmoothInterIntraCoverage; - continue; - } - - if (modeInfo.ReferenceFrames[1] <= Av1ReferenceFrameType.Intra) - { - continue; - } - - coverage |= modeInfo.CompoundType switch - { - Av1CompoundType.DistanceWeighted => DistanceWeightedCompoundCoverage, - Av1CompoundType.Wedge => modeInfo.CompoundWedgeSign - ? InvertedWedgeCompoundCoverage - : WedgeCompoundCoverage, - Av1CompoundType.DifferenceWeighted => modeInfo.DifferenceWeightedMaskType == Av1DifferenceWeightedMaskType.Type38Inverse - ? InvertedDifferenceWeightedCompoundCoverage - : DifferenceWeightedCompoundCoverage, - _ => 0, - }; - } - } - } - - return coverage; - } + private static int GetInterPredictionCoverage(Av1Decoder decoder) => + (int)decoder.DecodedInterPredictionFeatures; /// /// Verifies lossless syntax, residual reconstruction, and exact native samples against the independent scalar reference for @@ -3070,9 +3003,9 @@ public class Av1ReconstructionConformanceTests Assert.Equal(ScaledReferenceFixtureSize, frameBuffer.Height); Assert.Equal(Av1BitDepth.EightBit, frameBuffer.BitDepth); Assert.Equal(Av1ColorFormat.Yuv444, frameBuffer.ColorFormat); - Assert.Equal(1, frameBuffer.BufferY!.FastMemoryGroup.Count); - Assert.Equal(1, frameBuffer.BufferCb!.FastMemoryGroup.Count); - Assert.Equal(1, frameBuffer.BufferCr!.FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.Y).FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.U).FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.V).FastMemoryGroup.Count); ObuSequenceHeader sequenceHeader = Assert.IsType(decoder.SequenceHeader); ObuFrameHeader finalFrameHeader = Assert.IsType(decoder.FrameHeader); @@ -3200,9 +3133,9 @@ public class Av1ReconstructionConformanceTests Assert.Equal(ProgressiveFixtureHeight, frameBuffer.Height); Assert.Equal(Av1BitDepth.EightBit, frameBuffer.BitDepth); Assert.Equal(Av1ColorFormat.Yuv444, frameBuffer.ColorFormat); - Assert.Equal(1, frameBuffer.BufferY!.FastMemoryGroup.Count); - Assert.Equal(1, frameBuffer.BufferCb!.FastMemoryGroup.Count); - Assert.Equal(1, frameBuffer.BufferCr!.FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.Y).FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.U).FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.V).FastMemoryGroup.Count); ObuSequenceHeader sequenceHeader = Assert.IsType(decoder.SequenceHeader); ObuFrameHeader finalFrameHeader = Assert.IsType(decoder.FrameHeader); @@ -3665,46 +3598,7 @@ public class Av1ReconstructionConformanceTests Assert.False(decoder.FrameHeader.AllowIntraBlockCopy); Assert.Equal(0, GetPaletteCoverage(decoder)); - bool hasCodedResidual = false; - int superblockSizeLog2 = decoder.SequenceHeader.SuperblockSizeLog2; - int superblockColumnCount = Av1Math.AlignPowerOf2(decoder.SequenceHeader.MaxFrameWidth, superblockSizeLog2) >> superblockSizeLog2; - int superblockRowCount = Av1Math.AlignPowerOf2(decoder.SequenceHeader.MaxFrameHeight, superblockSizeLog2) >> superblockSizeLog2; - ReadOnlySpan planes = [Av1Plane.Y, Av1Plane.U, Av1Plane.V]; - for (int superblockRow = 0; superblockRow < superblockRowCount && !hasCodedResidual; superblockRow++) - { - for (int superblockColumn = 0; superblockColumn < superblockColumnCount && !hasCodedResidual; superblockColumn++) - { - Point superblock = new(superblockColumn, superblockRow); - foreach (Av1Plane plane in planes) - { - Span coefficients = plane switch - { - Av1Plane.Y => decoder.FrameInfo.GetCoefficientsY(superblock), - Av1Plane.U => decoder.FrameInfo.GetCoefficientsU(superblock), - _ => decoder.FrameInfo.GetCoefficientsV(superblock) - }; - - // Each transform reserves an end index followed by its coefficients. Any nonzero stored value - // proves that exact output traversed coefficient decoding, inverse quantization, and lossless WHT. - foreach (int coefficient in coefficients) - { - if (coefficient != 0) - { - hasCodedResidual = true; - break; - } - } - - if (hasCodedResidual) - { - break; - } - } - } - } - - Assert.True(hasCodedResidual); - + // Exact equality with independently decoded native planes proves the complete residual reconstruction contract. AssertNativePlanesEqual(decoder, frameBuffer, nativeReference); } @@ -3834,6 +3728,30 @@ public class Av1ReconstructionConformanceTests HeifMetadata metadata = image.Metadata.GetHeifMetadata(); Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod); Assert.Equal(metadataBitDepth, metadata.BitDepth); + + if (metadataBitDepth != HeifBitDepth.Bit8) + { + bool containsPrecisionBeyondEightBits = false; + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height && !containsPrecisionBeyondEightBits; y++) + { + Span row = accessor.GetRowSpan(y); + foreach (Rgba64 pixel in row) + { + // Expanding an eight-bit channel to ushort always produces a multiple of 257. At least one + // source-derived RGB channel must fall between those values to prove native precision survived. + if ((pixel.R % 257) != 0 || (pixel.G % 257) != 0 || (pixel.B % 257) != 0) + { + containsPrecisionBeyondEightBits = true; + break; + } + } + } + }); + + Assert.True(containsPrecisionBeyondEightBits, "The high-bit-depth presentation contains only eight-bit-expanded RGB samples."); + } } /// @@ -4102,16 +4020,18 @@ public class Av1ReconstructionConformanceTests /// A bit mask containing every selected loop-restoration filter type. private static int GetRestorationCoverage(Av1Decoder decoder) { + ObuSequenceHeader sequenceHeader = Assert.IsType(decoder.SequenceHeader); + Av1FrameInfo frameInfo = Assert.IsType(decoder.FrameInfo); int restorationCoverage = 0; - for (int plane = 0; plane < decoder.SequenceHeader!.ColorConfig.PlaneCount; plane++) + for (int plane = 0; plane < sequenceHeader.ColorConfig.PlaneCount; plane++) { - int rowCount = decoder.FrameInfo!.GetLoopRestorationUnitRowCount(plane); - int columnCount = decoder.FrameInfo.GetLoopRestorationUnitColumnCount(plane); + int rowCount = frameInfo.GetLoopRestorationUnitRowCount(plane); + int columnCount = frameInfo.GetLoopRestorationUnitColumnCount(plane); for (int row = 0; row < rowCount; row++) { for (int column = 0; column < columnCount; column++) { - Av1RestorationFilterType filterType = decoder.FrameInfo.GetLoopRestorationUnit(plane, row, column).FilterType; + Av1RestorationFilterType filterType = frameInfo.GetLoopRestorationUnit(plane, row, column).FilterType; if (filterType != Av1RestorationFilterType.None) { restorationCoverage |= 1 << (int)filterType; @@ -4300,55 +4220,6 @@ public class Av1ReconstructionConformanceTests int superblockSize = frameInfo.SuperblockModeInfoSize; Av1SuperblockInfo superblock = frameInfo.GetSuperblock(new Point(blockColumn / superblockSize, blockRow / superblockSize)); - Span transforms = superblock.GetTransformInfoY().Slice( - modeInfo.GetFirstTransformLocation(Av1Plane.Y), - modeInfo.GetTransformUnitCount(Av1Plane.Y)); - - Av1TransformInfo containingTransform = transforms[0]; - int containingTransformIndex = 0; - int transformColumn = modeInfoColumn - blockColumn; - int transformRow = modeInfoRow - blockRow; - for (int transformIndex = 0; transformIndex < transforms.Length; transformIndex++) - { - Av1TransformInfo transform = transforms[transformIndex]; - if (transformColumn >= transform.OffsetX && transformColumn < transform.OffsetX + transform.Size.Get4x4WideCount() - && transformRow >= transform.OffsetY && transformRow < transform.OffsetY + transform.Size.Get4x4HighCount()) - { - containingTransform = transform; - containingTransformIndex = transformIndex; - break; - } - } - - int superblockTransformIndex = modeInfo.GetFirstTransformLocation(Av1Plane.Y) + containingTransformIndex; - Span superblockTransforms = superblock.GetTransformInfoY(); - Span superblockCoefficients = superblock.CoefficientsY; - int coefficientOffset = 0; - for (int transformIndex = 0; transformIndex < superblockTransformIndex; transformIndex++) - { - if (superblockTransforms[transformIndex].CodeBlockFlag) - { - coefficientOffset += superblockCoefficients[coefficientOffset] + 1; - } - } - - StringBuilder coefficientDescription = new(); - if (containingTransform.CodeBlockFlag) - { - int coefficientCount = superblockCoefficients[coefficientOffset]; - coefficientDescription.Append(CultureInfo.InvariantCulture, $", quantized-coefficients={coefficientCount}:["); - for (int coefficientIndex = 0; coefficientIndex < coefficientCount; coefficientIndex++) - { - if (coefficientIndex != 0) - { - coefficientDescription.Append(','); - } - - coefficientDescription.Append(superblockCoefficients[coefficientOffset + coefficientIndex + 1]); - } - - coefficientDescription.Append(']'); - } ObuFrameHeader frameHeader = Assert.IsType(decoder.FrameHeader); int cdefUnitColumn = (modeInfoColumn % superblockSize) / CdefUnitModeInfoSize; @@ -4387,11 +4258,10 @@ public class Av1ReconstructionConformanceTests + $"filters={modeInfo.InterpolationFilters[0]}/{modeInfo.InterpolationFilters[1]}, motion={modeInfo.MotionMode}, " + $"filter-intra={modeInfo.UseFilterIntra}/{modeInfo.FilterIntraMode}, angle-delta={modeInfo.GetAngleDelta(plane)}, " + $"palette-size={modeInfo.GetPaletteSize(plane)}, transforms={modeInfo.GetTransformUnitCount(plane)}, " - + $"transform={containingTransform.Size}/{containingTransform.Type}/coded={containingTransform.CodeBlockFlag} " - + $"at ({containingTransform.OffsetX}, {containingTransform.OffsetY}), block-origin=({blockColumn}, {blockRow}). " + + $"block-origin=({blockColumn}, {blockRow}). " + $"Loop-filter={frameHeader.LoopFilterParameters.FilterLevel[0]}/{frameHeader.LoopFilterParameters.FilterLevel[1]}, " + $"sharpness={frameHeader.LoopFilterParameters.SharpnessLevel}, delta-q={frameHeader.DeltaQParameters.IsPresent}, " - + $"superblock-q={superblock.SuperblockQuantizerIndex}{coefficientDescription}, " + + $"superblock-q={superblock.SuperblockQuantizerIndex}, " + $"delta-lf={frameHeader.DeltaLoopFilterParameters.IsPresent}/{frameHeader.DeltaLoopFilterParameters.IsMulti}, " + $"CDEF={cdefStrengthIndex}/{cdefStrength}, restoration={frameHeader.LoopRestorationParameters.Items[0].Type}, " + $"film-grain={frameHeader.FilmGrainParameters.ApplyGrain}/" diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceFrameStoreTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceFrameStoreTests.cs index 6cd438ae8..dad60ee05 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceFrameStoreTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceFrameStoreTests.cs @@ -248,8 +248,11 @@ public class Av1ReferenceFrameStoreTests frameInfo.InitializeMotionField(configuration, sequenceHeader, frameHeader, sourceReferences); Assert.Equal(2, allocator.AllocationLog.Count); - Assert.Contains(allocator.AllocationLog, request => request.ElementType.Name == "RetainedMotionFieldEntry"); - Assert.Contains(allocator.AllocationLog, request => request.ElementType.Name == "TemporalMotionFieldEntry"); + TestMemoryAllocator.AllocationRequest retainedMotionField = + Assert.Single(allocator.AllocationLog, request => request.ElementType.Name == "RetainedMotionFieldEntry"); + + TestMemoryAllocator.AllocationRequest temporalMotionField = + Assert.Single(allocator.AllocationLog, request => request.ElementType.Name == "TemporalMotionFieldEntry"); using Av1ReferenceFrameStore store = new(); Av1ReferenceFrame frame = new( @@ -259,13 +262,13 @@ public class Av1ReferenceFrameStoreTests Assert.True(store.Commit(byte.MaxValue, frame, showFrame: true)); - // The reference frame owns the shared FrameInfo after the tile-reader lease ends. Physical reference slots - // and the shown-output pointer are aliases of that owner and must not release either motion field early. + // Full current-frame state owns the projected temporal field. The retained frame owns only the source field + // needed by later projections, irrespective of how many map and output aliases identify the same frame. frameInfo.Dispose(); - Assert.Empty(allocator.ReturnLog); + Assert.Single(allocator.ReturnLog, returned => returned.AllocationId == temporalMotionField.AllocationId); Av1ReferenceFrame output = store.TakeOutput(); - Assert.Empty(allocator.ReturnLog); + Assert.DoesNotContain(allocator.ReturnLog, returned => returned.AllocationId == retainedMotionField.AllocationId); output.Dispose(); output.Dispose(); @@ -314,6 +317,11 @@ public class Av1ReferenceFrameStoreTests { FrameType = ObuFrameType.InterFrame, OrderHint = 1, + FrameSize = new ObuFrameSize + { + FrameWidth = 64, + FrameHeight = 64 + }, ModeInfoColumnCount = 16, ModeInfoRowCount = 16, UseReferenceFrameMotionVectors = true @@ -341,18 +349,18 @@ public class Av1ReferenceFrameStoreTests } /// - /// Verifies that an eight-bit presentation copy contains every byte of each padded plane and the complete active geometry. + /// Verifies that an eight-bit presentation copy contains each visible plane without copying decoder padding. /// [Fact] - public void CopyToCopiesCompletePaddedEightBitFrame() - => ValidateCompleteFrameCopy(Av1BitDepth.EightBit); + public void CopyVisibleToCopiesEightBitPictureWithoutPadding() + => ValidateVisibleFrameCopy(Av1BitDepth.EightBit); /// - /// Verifies that a high-bit-depth presentation copy contains every native sample of each padded plane and the complete active geometry. + /// Verifies that a high-bit-depth presentation copy contains each visible plane without copying decoder padding. /// [Fact] - public void CopyToCopiesCompletePaddedHighBitDepthFrame() - => ValidateCompleteFrameCopy(Av1BitDepth.TwelveBit); + public void CopyVisibleToCopiesHighBitDepthPictureWithoutPadding() + => ValidateVisibleFrameCopy(Av1BitDepth.TwelveBit); /// /// Verifies that luma and subsampled chroma allocations cover the greatest legal unscaled UMV prediction extent. @@ -420,7 +428,7 @@ public class Av1ReferenceFrameStoreTests InitializeVisiblePlane( frameBuffer, - frameBuffer.BufferY!, + frameBuffer.GetPlaneBuffer(Av1Plane.Y), frameBuffer.OriginX, frameBuffer.OriginY, frameBuffer.Width, @@ -436,15 +444,15 @@ public class Av1ReferenceFrameStoreTests int chromaWidth = Av1Math.DivideLog2Ceiling(frameBuffer.Width, subX); int chromaHeight = Av1Math.DivideLog2Ceiling(frameBuffer.Height, subY); - InitializeVisiblePlane(frameBuffer, frameBuffer.BufferCb!, chromaOriginX, chromaOriginY, chromaWidth, chromaHeight, 1); - InitializeVisiblePlane(frameBuffer, frameBuffer.BufferCr!, chromaOriginX, chromaOriginY, chromaWidth, chromaHeight, 2); + InitializeVisiblePlane(frameBuffer, frameBuffer.GetPlaneBuffer(Av1Plane.U), chromaOriginX, chromaOriginY, chromaWidth, chromaHeight, 1); + InitializeVisiblePlane(frameBuffer, frameBuffer.GetPlaneBuffer(Av1Plane.V), chromaOriginX, chromaOriginY, chromaWidth, chromaHeight, 2); } Av1ReferenceFrameBorder.Extend(frameBuffer); AssertExtendedPlane( frameBuffer, - frameBuffer.BufferY!, + frameBuffer.GetPlaneBuffer(Av1Plane.Y), frameBuffer.OriginX, frameBuffer.OriginY, frameBuffer.Width, @@ -460,8 +468,8 @@ public class Av1ReferenceFrameStoreTests int chromaWidth = Av1Math.DivideLog2Ceiling(frameBuffer.Width, subX); int chromaHeight = Av1Math.DivideLog2Ceiling(frameBuffer.Height, subY); - AssertExtendedPlane(frameBuffer, frameBuffer.BufferCb!, chromaOriginX, chromaOriginY, chromaWidth, chromaHeight, 1); - AssertExtendedPlane(frameBuffer, frameBuffer.BufferCr!, chromaOriginX, chromaOriginY, chromaWidth, chromaHeight, 2); + AssertExtendedPlane(frameBuffer, frameBuffer.GetPlaneBuffer(Av1Plane.U), chromaOriginX, chromaOriginY, chromaWidth, chromaHeight, 1); + AssertExtendedPlane(frameBuffer, frameBuffer.GetPlaneBuffer(Av1Plane.V), chromaOriginX, chromaOriginY, chromaWidth, chromaHeight, 2); } } @@ -496,7 +504,7 @@ public class Av1ReferenceFrameStoreTests Av1FrameBuffer reconstructed = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); InitializeVisiblePlane( reconstructed, - reconstructed.BufferY!, + reconstructed.GetPlaneBuffer(Av1Plane.Y), reconstructed.OriginX, reconstructed.OriginY, reconstructed.Width, @@ -504,18 +512,18 @@ public class Av1ReferenceFrameStoreTests 0); Av1ReferenceFrameBorder.Extend(reconstructed); - Span reconstructedSamples = reconstructed.BufferY!.DangerousGetSingleSpan(); + Span reconstructedSamples = reconstructed.GetPlaneBuffer(Av1Plane.Y).DangerousGetSingleSpan(); byte[] ungrainedSamples = new byte[reconstructedSamples.Length]; reconstructedSamples.CopyTo(ungrainedSamples); Av1FrameBuffer presentation = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); - reconstructed.CopyTo(presentation); + reconstructed.CopyVisibleTo(presentation); Av1FilmGrainDecoder filmGrainDecoder = new(sequenceHeader, frameHeader, presentation); filmGrainDecoder.DecodeFrame(); - Av1FrameInfo frameInfo = new(sequenceHeader); + using Av1FrameInfo frameInfo = new(sequenceHeader); Av1ReferenceFrame retainedReference = new(reconstructed, frameHeader, frameInfo); - Av1ReferenceFrame grainedOutput = new(presentation, frameHeader, frameInfo); + Av1ReferenceFrame grainedOutput = new(presentation, frameHeader); Av1FrameBuffer retainedReferenceBuffer = retainedReference.FrameBuffer; using Av1ReferenceFrameStore store = new(); store.Commit(frameHeader.RefreshFrameFlags, retainedReference, showFrame: false); @@ -524,8 +532,8 @@ public class Av1ReferenceFrameStoreTests Assert.Same(retainedReference, store.Resolve(0)); Assert.Same(grainedOutput, store.OutputFrame); Assert.NotSame(retainedReference.FrameBuffer, grainedOutput.FrameBuffer); - Assert.True(ungrainedSamples.AsSpan().SequenceEqual(retainedReference.FrameBuffer.BufferY!.DangerousGetSingleSpan())); - Assert.False(ungrainedSamples.AsSpan().SequenceEqual(grainedOutput.FrameBuffer.BufferY!.DangerousGetSingleSpan())); + Assert.True(ungrainedSamples.AsSpan().SequenceEqual(retainedReference.FrameBuffer.GetPlaneBuffer(Av1Plane.Y).DangerousGetSingleSpan())); + Assert.False(ungrainedSamples.AsSpan().SequenceEqual(grainedOutput.FrameBuffer.GetPlaneBuffer(Av1Plane.Y).DangerousGetSingleSpan())); using Av1ReferenceFrame selectedOutput = store.TakeOutput(); @@ -537,10 +545,10 @@ public class Av1ReferenceFrameStoreTests } /// - /// Verifies a complete padded-plane copy for one native AV1 sample precision. + /// Verifies a visible-plane copy for one native AV1 sample precision. /// /// The coded sample precision. - private static void ValidateCompleteFrameCopy(Av1BitDepth bitDepth) + private static void ValidateVisibleFrameCopy(Av1BitDepth bitDepth) { ObuSequenceHeader sequenceHeader = CreateSequenceHeader(5, 3, bitDepth, false, true, true); using Av1FrameBuffer source = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false); @@ -552,18 +560,24 @@ public class Av1ReferenceFrameStoreTests source.Height = 2; source.MaxWidth = 4; source.MaxHeight = 2; - FillCompletePlane(source, source.BufferY!, 17); - FillCompletePlane(source, source.BufferCb!, 53); - FillCompletePlane(source, source.BufferCr!, 89); - destination.BufferY!.DangerousGetSingleSpan().Fill(0xA5); - destination.BufferCb!.DangerousGetSingleSpan().Fill(0xA5); - destination.BufferCr!.DangerousGetSingleSpan().Fill(0xA5); - - source.CopyTo(destination); - - Assert.True(source.BufferY!.DangerousGetSingleSpan().SequenceEqual(destination.BufferY!.DangerousGetSingleSpan())); - Assert.True(source.BufferCb!.DangerousGetSingleSpan().SequenceEqual(destination.BufferCb!.DangerousGetSingleSpan())); - Assert.True(source.BufferCr!.DangerousGetSingleSpan().SequenceEqual(destination.BufferCr!.DangerousGetSingleSpan())); + Buffer2D sourceY = source.GetPlaneBuffer(Av1Plane.Y); + Buffer2D sourceCb = source.GetPlaneBuffer(Av1Plane.U); + Buffer2D sourceCr = source.GetPlaneBuffer(Av1Plane.V); + Buffer2D destinationY = destination.GetPlaneBuffer(Av1Plane.Y); + Buffer2D destinationCb = destination.GetPlaneBuffer(Av1Plane.U); + Buffer2D destinationCr = destination.GetPlaneBuffer(Av1Plane.V); + FillCompletePlane(source, sourceY, 17); + FillCompletePlane(source, sourceCb, 53); + FillCompletePlane(source, sourceCr, 89); + destinationY.DangerousGetSingleSpan().Fill(0xA5); + destinationCb.DangerousGetSingleSpan().Fill(0xA5); + destinationCr.DangerousGetSingleSpan().Fill(0xA5); + + source.CopyVisibleTo(destination); + + AssertVisiblePlaneCopy(source, destination, Av1Plane.Y, 0, 0); + AssertVisiblePlaneCopy(source, destination, Av1Plane.U, 1, 1); + AssertVisiblePlaneCopy(source, destination, Av1Plane.V, 1, 1); Assert.Equal(source.StartPosition, destination.StartPosition); Assert.Equal(source.OriginX, destination.OriginX); Assert.Equal(source.OriginY, destination.OriginY); @@ -574,10 +588,49 @@ public class Av1ReferenceFrameStoreTests Assert.Equal(source.BitDepth, destination.BitDepth); Assert.Equal(source.ColorFormat, destination.ColorFormat); - byte sourceFirstSample = source.BufferY!.DangerousGetSingleSpan()[0]; + int visibleStorageOffset = (source.OriginY * sourceY.Width) + (source.OriginX * source.BytesPerSample); + byte sourceFirstVisibleByte = sourceY.DangerousGetSingleSpan()[visibleStorageOffset]; + + // Mutating a copied visible sample proves presentation ownership, not merely the already-untouched padding. + destinationY.DangerousGetSingleSpan()[visibleStorageOffset] ^= byte.MaxValue; + Assert.Equal(sourceFirstVisibleByte, sourceY.DangerousGetSingleSpan()[visibleStorageOffset]); + } + + /// + /// Verifies the copied visible rectangle and the untouched destination padding for one plane. + /// + /// The source frame. + /// The copied frame. + /// The plane to inspect. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + private static void AssertVisiblePlaneCopy( + Av1FrameBuffer source, + Av1FrameBuffer destination, + Av1Plane plane, + int subX, + int subY) + { + Buffer2D sourceBuffer = source.GetPlaneBuffer(plane); + Buffer2D destinationBuffer = destination.GetPlaneBuffer(plane); + int originX = (source.OriginX >> subX) * source.BytesPerSample; + int originY = source.OriginY >> subY; + int width = Av1Math.DivideLog2Ceiling(source.Width, subX) * source.BytesPerSample; + int height = Av1Math.DivideLog2Ceiling(source.Height, subY); + + for (int row = 0; row < destinationBuffer.Height; row++) + { + ReadOnlySpan sourceRow = sourceBuffer.DangerousGetRowSpan(row); + ReadOnlySpan destinationRow = destinationBuffer.DangerousGetRowSpan(row); + + for (int column = 0; column < destinationRow.Length; column++) + { + bool isVisible = row >= originY && row < originY + height && + column >= originX && column < originX + width; - destination.BufferY!.DangerousGetSingleSpan()[0] ^= byte.MaxValue; - Assert.Equal(sourceFirstSample, source.BufferY!.DangerousGetSingleSpan()[0]); + Assert.Equal(isVisible ? sourceRow[column] : (byte)0xA5, destinationRow[column]); + } + } } /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceMotionVectorsTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceMotionVectorsTests.cs index b4853d5b6..e8211599f 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceMotionVectorsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceMotionVectorsTests.cs @@ -87,7 +87,7 @@ public class Av1ReferenceMotionVectorsTests { ObuSequenceHeader sequenceHeader = CreateSequenceHeader(enableTemporalMotionVectors: false); ObuFrameHeader frameHeader = CreateFrameHeader(orderHint: 0, useReferenceFrameMotionVectors: false); - Av1FrameInfo frameInfo = new(sequenceHeader); + using Av1FrameInfo frameInfo = new(sequenceHeader); FillFrameWithIntraBlocks(frameInfo, sequenceHeader); Av1MotionVector nearest = new(8, 16); @@ -176,7 +176,7 @@ public class Av1ReferenceMotionVectorsTests globalMotion[5] = Av1GlobalMotionParameters.ModelScale; frameHeader.GetGlobalMotionParameters()[0] = globalMotion; - Av1FrameInfo frameInfo = new(sequenceHeader); + using Av1FrameInfo frameInfo = new(sequenceHeader); FillFrameWithIntraBlocks(frameInfo, sequenceHeader); Av1MotionVector decoded = new(40, -24); AddModeInfo( @@ -510,7 +510,17 @@ public class Av1ReferenceMotionVectorsTests /// Whether this frame consumes its projected temporal motion field. /// The configured frame header. private static ObuFrameHeader CreateFrameHeader(uint orderHint, bool useReferenceFrameMotionVectors) - => new() + { + ObuTileGroupHeader tilesInfo = new() + { + TileColumnCount = 1, + TileRowCount = 1, + }; + + tilesInfo.TileColumnStartModeInfo[1] = 32; + tilesInfo.TileRowStartModeInfo[1] = 32; + + return new() { FrameType = ObuFrameType.InterFrame, OrderHint = orderHint, @@ -518,14 +528,9 @@ public class Av1ReferenceMotionVectorsTests ModeInfoRowCount = 32, AllowHighPrecisionMotionVector = true, UseReferenceFrameMotionVectors = useReferenceFrameMotionVectors, - TilesInfo = new ObuTileGroupHeader - { - TileColumnCount = 1, - TileRowCount = 1, - TileColumnStartModeInfo = [0, 32], - TileRowStartModeInfo = [0, 32], - }, + TilesInfo = tilesInfo, }; + } /// /// Maps one intra block over each 64-by-64 superblock so every spatial search position has initialized mode information. @@ -617,7 +622,6 @@ public class Av1ReferenceMotionVectorsTests modeInfo.MotionVectors[0] = motionVector; modeInfo.MotionVectors[1] = secondaryMotionVector; frameInfo.UpdateModeInfo(modeInfo, superblockInfo); - superblockInfo.BlockCount++; return modeInfo; } diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceYuvConverter.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceYuvConverter.cs index c2e67201d..12d56d934 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceYuvConverter.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceYuvConverter.cs @@ -39,10 +39,9 @@ internal class Av1ReferenceYuvConverter // Constants used in BT.601 // private const double Wr = 0.299; // private const double Wb = 0.114; - private const double Wg = 1 - Wr - Wb; - public static Span RgbToYuv(Span row, bool normalized) + public static Span RgbToYuv(ReadOnlySpan row, bool normalized) { Rgb24[] result = new Rgb24[row.Length]; for (int i = 0; i < row.Length; i++) @@ -118,10 +117,6 @@ internal class Av1ReferenceYuvConverter byte greenByte = (byte)Math.Max(0, Math.Min(255, Math.Round(g))); byte blueByte = (byte)Math.Max(0, Math.Min(255, Math.Round(b))); - // Assert.True(Math.Abs(redByte - r) < 3, $"Red pixel out of byte range: {redByte} iso {r} from input Y={yuv[0]}, U={yuv[1]} and V={yuv[2]}."); - // Assert.True(Math.Abs(greenByte - g) < 3, $"Green pixel out of byte range: {greenByte} iso {g} from input Y={yuv[0]}, U={yuv[1]} and V={yuv[2]}."); - // Assert.True(Math.Abs(blueByte - b) < 3, $"Blue pixel out of byte range: {blueByte} iso {b} from input Y={yuv[0]}, U={yuv[1]} and V={yuv[2]}."); - result[i] = new Rgb24(redByte, greenByte, blueByte); } diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TemporalSegmentationTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TemporalSegmentationTests.cs index 97c87bf6e..c219738d5 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TemporalSegmentationTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TemporalSegmentationTests.cs @@ -128,7 +128,7 @@ public class Av1TemporalSegmentationTests const int modeInfoSize = 16; ObuSequenceHeader sequenceHeader = CreateSequenceHeader(64, 64); ObuFrameHeader primaryHeader = CreateFrameHeader(modeInfoSize, modeInfoSize, segmentationUpdateMap: 1, segmentationTemporalUpdate: 0); - Av1FrameInfo primaryFrameInfo = new(sequenceHeader); + using Av1FrameInfo primaryFrameInfo = new(sequenceHeader); primaryFrameInfo.InitializeSegmentIds(primaryHeader, null); primaryFrameInfo.SetSegmentId(Av1BlockSize.Block64x64, Point.Empty, 6); @@ -237,13 +237,20 @@ public class Av1TemporalSegmentationTests ObuSequenceHeader sequenceHeader = CreateSequenceHeader(64, 64); ObuFrameHeader currentHeader = CreateFrameHeader(16, 16, segmentationUpdateMap: 1, segmentationTemporalUpdate: 1); ObuFrameHeader primaryHeader = CreateFrameHeader(8, 16, segmentationUpdateMap: 1, segmentationTemporalUpdate: 0); - Av1FrameInfo currentFrameInfo = new(sequenceHeader); - Av1FrameInfo primaryFrameInfo = new(sequenceHeader); + using Av1FrameInfo currentFrameInfo = new(sequenceHeader); + using Av1FrameInfo primaryFrameInfo = new(sequenceHeader); currentFrameInfo.InitializeSegmentIds(currentHeader, null); primaryFrameInfo.InitializeSegmentIds(primaryHeader, null); primaryFrameInfo.SetSegmentId(Av1BlockSize.Block32x64, Point.Empty, 5); - int actual = currentFrameInfo.GetPredictedSegmentId(primaryFrameInfo, Av1BlockSize.Block16x16, Point.Empty); + Av1FrameBuffer primaryBuffer = + new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); + + using Av1ReferenceFrame primaryFrame = new(primaryBuffer, primaryHeader, primaryFrameInfo); + int actual = currentFrameInfo.GetPredictedSegmentId( + primaryFrame.ReferenceState, + Av1BlockSize.Block16x16, + Point.Empty); Assert.Equal(0, actual); } @@ -292,6 +299,14 @@ public class Av1TemporalSegmentationTests { ModeInfoColumnCount = modeInfoColumnCount, ModeInfoRowCount = modeInfoRowCount, + FrameSize = new ObuFrameSize + { + FrameWidth = modeInfoColumnCount << Av1Constants.ModeInfoSizeLog2, + FrameHeight = modeInfoRowCount << Av1Constants.ModeInfoSizeLog2, + SuperResolutionUpscaledWidth = modeInfoColumnCount << Av1Constants.ModeInfoSizeLog2, + RenderWidth = modeInfoColumnCount << Av1Constants.ModeInfoSizeLog2, + RenderHeight = modeInfoRowCount << Av1Constants.ModeInfoSizeLog2 + }, SegmentationParameters = new ObuSegmentationParameters { Enabled = true, diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs index ce1cbb48b..b3d1ac854 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; using SixLabors.ImageSharp.Memory; @@ -121,6 +122,14 @@ public class Av1TilingTests tileInfo.TileRowStartModeInfo[1] = sequenceHeader.SuperblockModeInfoSize; ObuFrameHeader frameHeader = new() { + FrameSize = new ObuFrameSize + { + FrameWidth = 64, + FrameHeight = 64, + SuperResolutionUpscaledWidth = 64, + RenderWidth = 64, + RenderHeight = 64 + }, ModeInfoColumnCount = sequenceHeader.SuperblockModeInfoSize, ModeInfoRowCount = sequenceHeader.SuperblockModeInfoSize, ModeInfoStride = sequenceHeader.SuperblockModeInfoSize, @@ -161,10 +170,26 @@ public class Av1TilingTests IAv1TileReader stub = new Av1TileDecoderStub(); ObuReader obuReader = new(); obuReader.ReadAll(ref bitStreamReader, dataSize, () => stub); - Av1FrameBuffer frameBuffer = new(Configuration.Default, obuReader.SequenceHeader, Av1ColorFormat.Yuv444, false); - Av1FrameInfo frameInfo = new(obuReader.SequenceHeader); - Av1FrameDecoder frameDecoder = new(obuReader.SequenceHeader, obuReader.FrameHeader, frameInfo, frameBuffer); - Av1TileReader tileReader = new(Configuration.Default, obuReader.SequenceHeader, obuReader.FrameHeader, frameDecoder); + using Av1ReferenceFrameStore referenceFrames = new(); + using Av1FrameBuffer frameBuffer = new( + Configuration.Default, + obuReader.SequenceHeader, + Av1ColorFormat.Yuv444, + false); + + using Av1FrameInfo frameInfo = new(obuReader.SequenceHeader); + using Av1FrameDecoder frameDecoder = new( + obuReader.SequenceHeader, + obuReader.FrameHeader, + frameInfo, + frameBuffer, + referenceFrames); + + using Av1TileReader tileReader = new( + Configuration.Default, + obuReader.SequenceHeader, + obuReader.FrameHeader, + frameDecoder); // Act tileReader.ReadTile(tileSpan, 0); @@ -194,10 +219,21 @@ public class Av1TilingTests // Reuse known-good tile syntax after parsing so this test isolates native high-bit prediction and reconstruction wiring. obuReader.SequenceHeader.ColorConfig.BitDepth = (Av1BitDepth)bitDepthIndex; + using Av1ReferenceFrameStore referenceFrames = new(); using Av1FrameBuffer frameBuffer = new(Configuration.Default, obuReader.SequenceHeader, Av1ColorFormat.Yuv444, false); - Av1FrameInfo frameInfo = new(obuReader.SequenceHeader); - Av1FrameDecoder frameDecoder = new(obuReader.SequenceHeader, obuReader.FrameHeader, frameInfo, frameBuffer); - Av1TileReader tileReader = new(Configuration.Default, obuReader.SequenceHeader, obuReader.FrameHeader, frameDecoder); + using Av1FrameInfo frameInfo = new(obuReader.SequenceHeader); + using Av1FrameDecoder frameDecoder = new( + obuReader.SequenceHeader, + obuReader.FrameHeader, + frameInfo, + frameBuffer, + referenceFrames); + + using Av1TileReader tileReader = new( + Configuration.Default, + obuReader.SequenceHeader, + obuReader.FrameHeader, + frameDecoder); tileReader.ReadTile(tileSpan, 0); @@ -220,10 +256,12 @@ public class Av1TilingTests IAv1TileReader stub = new Av1TileDecoderStub(); ObuReader obuReader = new(); obuReader.ReadAll(ref bitStreamReader, dataSize, () => stub); - Av1FrameBuffer frameBuffer = new(Configuration.Default, obuReader.SequenceHeader, Av1ColorFormat.Yuv444, false); - Av1FrameInfo frameInfo = new(obuReader.SequenceHeader); Av1FrameDecoderStub frameDecoder = new(); - Av1TileReader tileReader = new(Configuration.Default, obuReader.SequenceHeader, obuReader.FrameHeader, frameDecoder); + using Av1TileReader tileReader = new( + Configuration.Default, + obuReader.SequenceHeader, + obuReader.FrameHeader, + frameDecoder); // Act tileReader.ReadTile(tileSpan, 0); @@ -247,7 +285,7 @@ public class Av1TilingTests IAv1TileReader stub = new Av1TileDecoderStub(); ObuReader obuReader = new(); obuReader.ReadAll(ref bitStreamReader, dataSize, () => stub); - Av1TileReader tileReader = new(Configuration.Default, obuReader.SequenceHeader, obuReader.FrameHeader); + using Av1TileReader tileReader = new(Configuration.Default, obuReader.SequenceHeader, obuReader.FrameHeader); tileReader.ReadTile(tileSpan, 0); @@ -259,7 +297,7 @@ public class Av1TilingTests { Point superblockPosition = new(column / superblockSize, row / superblockSize); Av1SuperblockInfo superblockInfo = tileReader.FrameInfo.GetSuperblock(superblockPosition); - Span modeInfos = superblockInfo.GetModeInfos(); + Av1FrameInfo.ModeInfoCollection modeInfos = superblockInfo.GetModeInfos(); Assert.Equal(superblockInfo.BlockCount, modeInfos.Length); Assert.Equal(modeInfos[0].ModeInfoIndex, tileReader.FrameInfo.GetModeInfo(superblockPosition).ModeInfoIndex); @@ -288,75 +326,6 @@ public class Av1TilingTests Assert.True(parsedModeInfoCount > 16); } - [Fact] - public void ParsedCoefficientsRemainAvailablePerSuperblockAndPlane() - { - string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Heif.XnConvert); - byte[] content = File.ReadAllBytes(filePath); - const int dataOffset = 0x010E; - const int dataSize = 0x03CC; - const int tileOffset = 18; - Span headerSpan = content.AsSpan(dataOffset, dataSize); - Span tileSpan = content.AsSpan(dataOffset + tileOffset, dataSize - tileOffset); - Av1BitStreamReader bitStreamReader = new(headerSpan); - IAv1TileReader stub = new Av1TileDecoderStub(); - ObuReader obuReader = new(); - obuReader.ReadAll(ref bitStreamReader, dataSize, () => stub); - Av1TileReader tileReader = new(Configuration.Default, obuReader.SequenceHeader, obuReader.FrameHeader); - - tileReader.ReadTile(tileSpan, 0); - - int codedTransformCount = 0; - int superblockSize = obuReader.SequenceHeader.SuperblockModeInfoSize; - for (int row = 0; row < obuReader.FrameHeader.ModeInfoRowCount; row += superblockSize) - { - for (int column = 0; column < obuReader.FrameHeader.ModeInfoColumnCount; column += superblockSize) - { - Point superblockPosition = new(column / superblockSize, row / superblockSize); - Av1SuperblockInfo superblockInfo = tileReader.FrameInfo.GetSuperblock(superblockPosition); - int[] coefficientIndices = new int[Av1Constants.MaxPlanes]; - - foreach (Av1BlockModeInfo modeInfo in superblockInfo.GetModeInfos()) - { - Point modeInfoPosition = new(column + modeInfo.PositionInSuperblock.X, row + modeInfo.PositionInSuperblock.Y); - bool hasChroma = Av1TileReader.HasChroma(obuReader.SequenceHeader, modeInfoPosition, modeInfo.BlockSize); - - for (int plane = 0; plane < obuReader.SequenceHeader.ColorConfig.PlaneCount; plane++) - { - if (plane != 0 && !hasChroma) - { - continue; - } - - int transformUnitCount = modeInfo.GetTransformUnitCount((Av1Plane)plane); - int transformInfoIndex = modeInfo.GetFirstTransformLocation((Av1Plane)plane); - if (plane == (int)Av1Plane.V) - { - transformInfoIndex += transformUnitCount; - } - - Span transformInfos = superblockInfo.GetTransformInfo(plane)[transformInfoIndex..]; - Span coefficients = superblockInfo.GetCoefficients((Av1Plane)plane); - for (int i = 0; i < transformUnitCount; i++) - { - if (!transformInfos[i].CodeBlockFlag) - { - continue; - } - - int endOfBlock = coefficients[coefficientIndices[plane]]; - Assert.InRange(endOfBlock, 1, transformInfos[i].Size.GetWidth() * transformInfos[i].Size.GetHeight()); - coefficientIndices[plane] += endOfBlock + 1; - codedTransformCount++; - } - } - } - } - } - - Assert.True(codedTransformCount > 3); - } - [Theory] [InlineData(TestImages.Heif.XnConvert, 0x010E, 0x03CC, 18, 16)] [InlineData(TestImages.Heif.Orange4x4, 0x010E, 0x001d, 21, 1)] diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterPredictorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TranslationalInterPredictorTests.cs similarity index 94% rename from tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterPredictorTests.cs rename to tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TranslationalInterPredictorTests.cs index 19a4a545d..0ccd072a1 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterPredictorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TranslationalInterPredictorTests.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; /// Verifies AV1 translational inter prediction against an independent implementation of the normative fixed-point convolution rules. /// [Trait("Format", "Avif")] -public class Av1InterPredictorTests +public class Av1TranslationalInterPredictorTests { /// /// The number of fractional coefficient bits in AV1 interpolation kernels. @@ -125,13 +125,33 @@ public class Av1InterPredictorTests ApplyReference(source, sourceStride, sourceOrigin, expected, DestinationPrefix, destinationStride, testCase, 8); - Av1InterPredictor.Predict( - source, sourceStride, sourceOrigin, actual.AsSpan(DestinationPrefix), destinationStride, testCase.Width, testCase.Height, - testCase.HorizontalFilter, testCase.VerticalFilter, testCase.HorizontalPhase, testCase.VerticalPhase, simdScratch); + Av1TranslationalInterPredictor.Predict( + source, + sourceStride, + sourceOrigin, + actual.AsSpan(DestinationPrefix), + destinationStride, + testCase.Width, + testCase.Height, + testCase.HorizontalFilter, + testCase.VerticalFilter, + testCase.HorizontalPhase, + testCase.VerticalPhase, + simdScratch); - Av1InterPredictor.PredictScalar( - source, sourceStride, sourceOrigin, scalar.AsSpan(DestinationPrefix), destinationStride, testCase.Width, testCase.Height, - testCase.HorizontalFilter, testCase.VerticalFilter, testCase.HorizontalPhase, testCase.VerticalPhase, scalarScratch); + Av1TranslationalInterPredictor.PredictScalar( + source, + sourceStride, + sourceOrigin, + scalar.AsSpan(DestinationPrefix), + destinationStride, + testCase.Width, + testCase.Height, + testCase.HorizontalFilter, + testCase.VerticalFilter, + testCase.HorizontalPhase, + testCase.VerticalPhase, + scalarScratch); AssertEqual(expected, actual, testCase, "SIMD-first byte"); AssertEqual(expected, scalar, testCase, "scalar byte"); @@ -158,13 +178,35 @@ public class Av1InterPredictorTests ApplyReference(source, sourceStride, sourceOrigin, expected, DestinationPrefix, destinationStride, testCase, bitDepth); - Av1InterPredictor.Predict( - source, sourceStride, sourceOrigin, actual.AsSpan(DestinationPrefix), destinationStride, testCase.Width, testCase.Height, - testCase.HorizontalFilter, testCase.VerticalFilter, testCase.HorizontalPhase, testCase.VerticalPhase, bitDepth, simdScratch); - - Av1InterPredictor.PredictScalar( - source, sourceStride, sourceOrigin, scalar.AsSpan(DestinationPrefix), destinationStride, testCase.Width, testCase.Height, - testCase.HorizontalFilter, testCase.VerticalFilter, testCase.HorizontalPhase, testCase.VerticalPhase, bitDepth, scalarScratch); + Av1TranslationalInterPredictor.Predict( + source, + sourceStride, + sourceOrigin, + actual.AsSpan(DestinationPrefix), + destinationStride, + testCase.Width, + testCase.Height, + testCase.HorizontalFilter, + testCase.VerticalFilter, + testCase.HorizontalPhase, + testCase.VerticalPhase, + bitDepth, + simdScratch); + + Av1TranslationalInterPredictor.PredictScalar( + source, + sourceStride, + sourceOrigin, + scalar.AsSpan(DestinationPrefix), + destinationStride, + testCase.Width, + testCase.Height, + testCase.HorizontalFilter, + testCase.VerticalFilter, + testCase.HorizontalPhase, + testCase.VerticalPhase, + bitDepth, + scalarScratch); AssertEqual(expected, actual, testCase, $"SIMD-first {bitDepth}-bit ushort"); AssertEqual(expected, scalar, testCase, $"scalar {bitDepth}-bit ushort"); @@ -406,8 +448,16 @@ public class Av1InterPredictorTests ReadOnlySpan verticalCoefficients = GetCoefficients(testCase.VerticalFilter, testCase.VerticalPhase, testCase.Height <= 4); ApplyTwoDimensionalReference( - source, sourceStride, sourceOrigin, destination, destinationOrigin, destinationStride, testCase, - horizontalCoefficients, verticalCoefficients, bitDepth); + source, + sourceStride, + sourceOrigin, + destination, + destinationOrigin, + destinationStride, + testCase, + horizontalCoefficients, + verticalCoefficients, + bitDepth); } /// @@ -484,8 +534,16 @@ public class Av1InterPredictorTests ReadOnlySpan verticalCoefficients = GetCoefficients(testCase.VerticalFilter, testCase.VerticalPhase, testCase.Height <= 4); ApplyTwoDimensionalReference( - source, sourceStride, sourceOrigin, destination, destinationOrigin, destinationStride, testCase, - horizontalCoefficients, verticalCoefficients, bitDepth); + source, + sourceStride, + sourceOrigin, + destination, + destinationOrigin, + destinationStride, + testCase, + horizontalCoefficients, + verticalCoefficients, + bitDepth); } /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs index e54ff09b9..159b0fef0 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs @@ -254,7 +254,7 @@ public class Av1YuvConverterTests // Assert Assert.Equal(2, frameBuffer.BytesPerSample); Assert.Equal(3 + (frameBuffer.OriginX * 2), stride); - Assert.Equal(stride * 2, frameBuffer.BufferY!.Width); + Assert.Equal(stride * 2, frameBuffer.GetPlaneBuffer(Av1Plane.Y).Width); Assert.Equal(321, frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, 0, 0, 0)[0]); Assert.Equal(2, chromaRow.Length); } @@ -509,7 +509,7 @@ public class Av1YuvConverterTests Span vRow = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0); for (int i = 0; i < frameBuffer.Width; i++) { - Rgb24 pixel = new(); + Rgb24 pixel = default; pixel.R = yRow[i]; pixel.G = uRow[i]; pixel.B = vRow[i]; @@ -553,7 +553,7 @@ public class Av1YuvConverterTests /// The independently converted reference pixels. /// The pixels produced by the implementation under test. /// The permitted absolute component difference. - private static void Compare(Span referenceOutput, Span actual, int allowedDifference) + private static void Compare(ReadOnlySpan referenceOutput, ReadOnlySpan actual, int allowedDifference) { for (int i = 0; i < actual.Length; i++) { @@ -1006,7 +1006,7 @@ public class Av1YuvConverterTests /// The source test-image provider. // [Theory] // [WithFile(TestImages.Jpeg.Baseline.Winter444_Interleaved, PixelTypes.Rgb24)] - public void RoundTrip(TestImageProvider provider) + public static void RoundTrip(TestImageProvider provider) { // Assign using Image image = provider.GetImage(); diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs index b2fdc0759..2a6d23ab7 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs @@ -499,6 +499,48 @@ public class ObuFrameHeaderTests Assert.Equal(bitStream.Length * 8, reader.BitPosition); } + /// + /// Verifies that content-light metadata is parsed from the OBU and retained for image metadata transfer. + /// + [Fact] + public void ReadMetadataRetainsContentLightLevel() + { + // Metadata type 1 is followed by big-endian MaxCLL and MaxFALL values and byte-aligned trailing bits. + byte[] bitStream = [0x2A, 0x06, 0x01, 0x03, 0xE8, 0x01, 0x90, 0x80]; + Av1BitStreamReader reader = new(bitStream); + ObuReader obuReader = new(); + IAv1TileReader tileDecoder = new Av1TileDecoderStub(); + + obuReader.ReadAll(ref reader, bitStream.Length, tileDecoder); + + Assert.True(obuReader.ContentLightLevel.HasValue); + Assert.Equal((ushort)1_000, obuReader.ContentLightLevel.Value.MaximumContentLightLevel); + Assert.Equal((ushort)400, obuReader.ContentLightLevel.Value.MaximumPictureAverageLightLevel); + Assert.Equal(bitStream.Length * 8, reader.BitPosition); + } + + /// + /// Verifies that fixed-length metadata without its required trailing one bit is rejected. + /// + [Fact] + public void ReadMetadataRejectsMissingTrailingBits() + { + byte[] bitStream = [0x2A, 0x05, 0x01, 0x03, 0xE8, 0x01, 0x90]; + + Assert.Throws(() => ReadObuStream(bitStream)); + } + + /// + /// Verifies that the unsupported tile-list OBU is rejected instead of being treated as ignorable data. + /// + [Fact] + public void ReadTileListRejectsUnsupportedSyntax() + { + byte[] bitStream = [0x42, 0x01, 0x80]; + + Assert.Throws(() => ReadObuStream(bitStream)); + } + /// /// Verifies that a four-byte tile size cannot wrap into an empty first tile. /// @@ -586,7 +628,9 @@ public class ObuFrameHeaderTests public void ReadTemporalDelimiterAllowsZeroPayloadPadding() { byte[] bitStream = [0x12, 0x02, 0x00, 0x00]; - ReadObuStream(bitStream); + Exception exception = Record.Exception(() => ReadObuStream(bitStream)); + + Assert.Null(exception); } [Fact] diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameLifecycleTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameLifecycleTests.cs index 442e58a55..727061d78 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameLifecycleTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameLifecycleTests.cs @@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; @@ -92,6 +93,32 @@ public class ObuFrameLifecycleTests Assert.Equal(ProgressiveImageHeight, secondFrameHeader.FrameSize.RenderHeight); } + /// + /// Verifies that production existing-frame presentation needs no tile reader, frame buffer, or reconstruction graph. + /// + [Fact] + public void SequenceDecoderPresentsExistingFrameWithoutTileState() + { + byte[] bitStream = [.. ProgressiveTwoFrameObuStream]; + + // A standalone frame-header OBU selects retained slot zero. The payload contains show_existing_frame, the + // three-bit slot index, and the required trailing-one bit; no tile-group OBU follows it. + byte[] showExistingFrame = [0x1A, 0x01, 0x88]; + using Av1Decoder decoder = new(Configuration.Default, ProgressiveOperatingPointIndex); + using ImageFrame reconstructed = decoder.DecodeSequenceFrame(bitStream, null, null); + using ImageFrame existing = decoder.DecodeSequenceFrame(showExistingFrame, null, null); + + Assert.Equal(reconstructed.Size, existing.Size); + for (int row = 0; row < reconstructed.Height; row++) + { + Assert.True( + reconstructed.PixelBuffer.DangerousGetRowSpan(row) + .SequenceEqual(existing.PixelBuffer.DangerousGetRowSpan(row))); + } + + Assert.Null(decoder.FrameInfo); + } + /// /// Verifies that invalid trailing data in a subsequent sequence header is rejected before that sequence can create frame state. /// @@ -396,7 +423,7 @@ public class ObuFrameLifecycleTests } } - Av1FrameInfo frameInfo = new(sequenceHeader); + using Av1FrameInfo frameInfo = new(sequenceHeader); Av1FrameBuffer frameBuffer = new( Configuration.Default, sequenceHeader, diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs index 9ce524585..18b3be9e2 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Heif; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Tests.ColorProfiles.Icc; @@ -115,9 +116,10 @@ public class HeifDecoderTests using Image expectedPreserved = Image.Load(preserveOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes); using Image expected = Image.Load(convertOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes); - Assert.NotNull(preserved.Metadata.IccProfile); + IccProfile preservedIccProfile = Assert.IsType(preserved.Metadata.IccProfile); + IccProfile expectedIccProfile = Assert.IsType(expectedPreserved.Metadata.IccProfile); Assert.Null(converted.Metadata.IccProfile); - Assert.Equal(expectedPreserved.Metadata.IccProfile!.ToByteArray(), preserved.Metadata.IccProfile.ToByteArray()); + Assert.Equal(expectedIccProfile.ToByteArray(), preservedIccProfile.ToByteArray()); Assert.NotEmpty(ImageComparer.Exact.CompareImages(preserved, converted)); ImageComparer.TolerantPercentage(1F, 20).VerifySimilarity(expected, converted); } @@ -140,9 +142,10 @@ public class HeifDecoderTests Assert.Equal(2, preserved.Frames.Count); Assert.Equal(preserved.Frames.Count, converted.Frames.Count); - Assert.NotNull(preserved.Metadata.IccProfile); + IccProfile preservedIccProfile = Assert.IsType(preserved.Metadata.IccProfile); + IccProfile expectedIccProfile = Assert.IsType(expectedPreserved.Metadata.IccProfile); Assert.Null(converted.Metadata.IccProfile); - Assert.Equal(expectedPreserved.Metadata.IccProfile!.ToByteArray(), preserved.Metadata.IccProfile.ToByteArray()); + Assert.Equal(expectedIccProfile.ToByteArray(), preservedIccProfile.ToByteArray()); for (int i = 0; i < converted.Frames.Count; i++) { diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs index 99f77f654..cffc34fa0 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs @@ -606,14 +606,19 @@ public class HeifSequenceParserTests HeifSequenceTrack track = parser.Parse(stream, GetMoviePayloadLength(data)).ColorTrack; Assert.NotNull(track.CicpProfile); - Assert.Equal(SyntheticHorizontalPixelSpacing, track.PixelAspectRatio!.HorizontalSpacing); - Assert.Equal(SyntheticVerticalPixelSpacing, track.PixelAspectRatio.VerticalSpacing); + HeifPixelAspectRatio pixelAspectRatio = Assert.IsType(track.PixelAspectRatio); + Assert.Equal(SyntheticHorizontalPixelSpacing, pixelAspectRatio.HorizontalSpacing); + Assert.Equal(SyntheticVerticalPixelSpacing, pixelAspectRatio.VerticalSpacing); Size codedSize = new(SyntheticWidth, SyntheticHeight); - Assert.Equal(new Rectangle(Point.Empty, codedSize), track.CleanAperture!.Value.ToRectangle(codedSize)); + Assert.True(track.CleanAperture.HasValue); + HeifCleanAperture cleanAperture = track.CleanAperture.GetValueOrDefault(); + Assert.Equal(new Rectangle(Point.Empty, codedSize), cleanAperture.ToRectangle(codedSize)); Assert.Equal((byte)1, track.RotationAngle); Assert.Equal((byte)1, track.MirrorAxis); - Assert.Equal(SyntheticMaximumContentLightLevel, track.ContentLightLevel!.Value.MaximumContentLightLevel); + Assert.True(track.ContentLightLevel.HasValue); + HeifContentLightLevel contentLightLevel = track.ContentLightLevel.GetValueOrDefault(); + Assert.Equal(SyntheticMaximumContentLightLevel, contentLightLevel.MaximumContentLightLevel); Assert.NotNull(track.MasteringDisplayColorVolume); Assert.NotNull(track.ContentColorVolume); Assert.NotNull(track.AmbientViewingEnvironment); diff --git a/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs b/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs index 1f5b68bb1..099be2557 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Numerics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; @@ -91,7 +92,7 @@ internal class TestMemoryAllocator : MemoryAllocator public static AllocationRequest Create(AllocationOptions allocationOptions, int length, T[] buffer, int allocationId) { Type type = typeof(T); - int elementSize = Marshal.SizeOf(type); + int elementSize = Unsafe.SizeOf(); return new AllocationRequest(type, allocationOptions, length, length * elementSize, allocationId, buffer.GetHashCode()); }