diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 5a16a779b7..2192a0f567 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -77,6 +77,7 @@ This snapshot pins or classifies the available references and failures; it does | `Av1TileReader` loop-restoration unit syntax, `Av1SymbolDecoder` restoration distributions/subexponential codes, and `Av1FrameInfo` unit storage | AV1 section 5.11.57 `read_lr` and `read_lr_unit` syntax | libaom `av1/decoder/decodeframe.c`, `av1/common/restoration.c`, `av1/common/restoration.h`, `av1/common/entropymode.c`, `aom_dsp/binary_codes_reader.c`, and `aom_dsp/recenter.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Decode tile-local switchable/Wiener/self-guided selections, finite reference-subexponential coefficients, chroma Wiener windows, self-guided parameter sets, super-resolution-adjusted unit corners, and the AV1 nearest-unit-count rule into frame-owned per-plane grids. This is compressed still-image syntax and adds no movie, track, timing, fragment, audio, or sequence surface. | | `Av1WienerFilter` | AV1 sections 7.17.4 and 7.17.5 Wiener restoration filtering and coefficient derivation | libaom `av1/common/restoration.c`, `av1/common/restoration.h`, `av1/common/convolve.c`, and `av1/common/convolve.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve the implicit center-sample contribution, separable horizontal/vertical rounding, bit-depth-dependent 16-bit intermediate range, and final 8/10/12-bit clipping. Reuse `Vector128_.MultiplyAddAdjacent` for the contiguous horizontal eight-tap product with an exact scalar fallback. Keep this scalar/SIMD oracle disabled until restoration stripe boundaries and self-guided filtering are both complete. | | `Av1SelfGuidedFilter` | AV1 sections 7.17.2 and 7.17.3 self-guided and box-filter processes | libaom `av1/common/restoration.c` and `av1/common/restoration.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve the sixteen normative radius/variance parameter sets, local mean and variance normalization, alternating-row radius-two optimization, decoded projection-coefficient behavior, signed rounding, and 8/10/12-bit clipping. Use caller-owned scratch storage and a scalar oracle before considering a managed SIMD translation of libaom's architecture-specific nonlinear filter kernels. Keep this image-reconstruction stage disabled until restoration stripe boundaries are complete. | +| `Av1LoopRestorationBoundary`, `Av1LoopRestorationDecoder`, and `Av1FrameDecoder` restoration-stage ordering | AV1 section 7.17 loop restoration, including striped boundary semantics | libaom `av1/common/restoration.c`, `av1/common/restoration.h`, `av1/common/resize.c`, and `av1/decoder/decodeframe.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve two deblocked rows at internal 64-luma stripe boundaries before CDEF, apply the existing normative SIMD-backed super-resolution kernel to saved rows when scaled, use post-CDEF/super-resolution samples at frame edges, extend the final restoration unit up to 150 percent of nominal size, and filter from immutable plane snapshots into separate output planes. This is bounded still-image reconstruction state, not retained reference-frame, track, timing, or playback state. | | `Av1FrameInfo`, `Av1TileReader`, and `Av1BlockDecoder` transform/coefficient storage | AV1 section 5.11.39 coefficient syntax and section 7.11.2 reconstruction | libaom `av1/decoder/decodetxb.c` and `av1/decoder/decoder.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve separate luma and chroma transform coefficients at monotonically advancing per-plane offsets within each superblock so reconstruction consumes the same transform-block order produced by tile parsing. | | `Av1InverseQuantizer` and `Av1InverseQuantizationLookup` | AV1 section 7.12.3 inverse quantization | libaom `aom_dsp/aom_dsp_common.h`, `av1/common/quant_common.c`, and `av1/decoder/decodetxb.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Select the per-segment matrix level, alias 64-pixel transform dimensions to their adjusted matrices, retain a flat level-15 matrix, and apply the five-bit inverse-matrix weight scale. The large managed lookup remains a single process-wide table. | | `Av1Inverse2dTransformer` and `Av1InverseTransformerFactory` | AV1 section 7.11.2 inverse transform and reconstruction | libaom `av1/common/av1_inv_txfm1d.c`, `av1/common/av1_inv_txfm2d.c`, and `av1/common/idct.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Scalar transform oracle for coefficient-row traversal, intermediate layout, stage ranges, clipping, and high-bit-depth sample addition. The managed 16-bit overload is also used as a parity oracle for the byte overload. | @@ -124,13 +125,13 @@ This assessment is based on the current source after the upstream ImageSharp mer ### AV1 decoder - The single-still `Av1Decoder` path now parses tile state before allocating and reconstructing one independently decodable frame, and it disposes the reconstruction planes after pixel conversion. It deliberately does not retain animation/video reference frames or implement `show_existing_frame` playback state. -- The reconstruction pipeline now records plane-relative transform geometry, preserves tile-local delta-Q and delta-LF predictors, derives segmentation and reference-adjusted filter levels, and runs the exact scalar low/high-bit-depth AV1 4-, 6-, 8-, and 14-tap deblocking kernels in normative vertical-then-horizontal order. It then applies scalar CDEF direction search, luma variance adjustment, primary and secondary constrained taps, chroma direction conversion, high-bit-depth scaling, skipped-block selection, and frame-edge sentinel handling from immutable per-plane snapshots. Active super-resolution now derives the Appendix A bounded coded width and applies the exact 64-phase, 8-tap horizontal filter with aligned reconstruction-edge input, 8/10/12-bit clipping, and the existing cross-platform `Vector128_.MultiplyAddAdjacent` helper. The available full-decode AV1 fixture does not exercise filtered transform edges, active CDEF strengths, or super-resolution, so independent 8-, 10-, and 12-bit AVIF vectors remain required before these stages have external pixel-level verification. Loop restoration and padding remain disabled even though they are normative when signaled, not optional quality improvements. +- The reconstruction pipeline now records plane-relative transform geometry, preserves tile-local delta-Q and delta-LF predictors, derives segmentation and reference-adjusted filter levels, and runs the exact scalar low/high-bit-depth AV1 4-, 6-, 8-, and 14-tap deblocking kernels in normative vertical-then-horizontal order. It then applies scalar CDEF direction search, luma variance adjustment, primary and secondary constrained taps, chroma direction conversion, high-bit-depth scaling, skipped-block selection, and frame-edge sentinel handling from immutable per-plane snapshots. Active super-resolution derives the Appendix A bounded coded width and applies the exact 64-phase, 8-tap horizontal filter with aligned reconstruction-edge input, 8/10/12-bit clipping, and the existing cross-platform `Vector128_.MultiplyAddAdjacent` helper. Loop restoration now follows super-resolution, preserves the required pre-CDEF deblocked context at internal stripes, and applies decoded Wiener or self-guided units from immutable plane snapshots. Independent 8-, 10-, and 12-bit AVIF vectors exercising every active filter stage remain required before these paths have external pixel-level verification. - Palette mode now reads the normative luma/chroma mode and size CDFs, neighbor color caches, high-bit-depth color syntax, diagonal color-index maps, clipped-edge padding, and direct sample prediction through the existing reconstruction pipeline. The scalar implementation matches the pinned libaom source, but an independently encoded palette AVIF fixture is still required before this path is independently verified. -- Loop-restoration unit parsing now records tile-local switchable/Wiener/self-guided filter selections and coefficients in frame-owned plane grids, including super-resolution-adjusted unit corners and the corrected conditional 64x64-superblock unit-size bit. The normative Wiener/self-guided filtering and deblocked stripe-boundary preservation are still disabled, so active loop-restoration images are not yet complete. Other normative independently decodable still-image syntax paths still contain `NotImplementedException` or equivalent unsupported branches. Tile-local palette CDF adaptation is now present; the remaining still-image frame-context behavior requires a separate source audit without introducing sequence playback state. +- Loop-restoration unit parsing records tile-local switchable/Wiener/self-guided filter selections and coefficients in frame-owned plane grids, including super-resolution-adjusted unit corners and the corrected conditional 64x64-superblock unit-size bit. The active restoration stage implements the normative unit geometry, striped deblocked boundaries, Wiener filtering, self-guided projection, and 8/10/12-bit clipping, while reusing the existing SIMD-backed super-resolution and adjacent multiply/add primitives. Independently encoded fixtures covering every parameter set, plane layout, bit depth, and frame-edge geometry are still required. Other normative independently decodable still-image syntax paths still contain `NotImplementedException` or equivalent unsupported branches. Tile-local palette CDF adaptation is present; the remaining still-image frame-context behavior requires a separate source audit without introducing sequence playback state. - The frame buffer now establishes two-byte native sample storage, logical plane rows, and sample-unit block strides for 10/12-bit frames. The active intra-prediction, inverse-transform, and block-reconstruction path selects native 16-bit samples for 10/12-bit frames and has focused pipeline wiring coverage. Independently encoded high-bit-depth AVIF conformance files are still required; chroma-from-luma storage and the other normative reconstruction stages listed below remain incomplete. - `Av1YuvConverter` now consumes the signaled range, supported H.273 matrix coefficients, subsampling, and chroma sample position for 8, 10, and 12-bit output. Its high-bit-depth decode and encode paths use allocator-backed `Rgb48` rows and the existing `PixelOperations` conversions, avoiding the former eight-bit intermediate. Encoder conversion covers monochrome, YUV 4:2:0, 4:2:2, and 4:4:4 with libavif-compatible box averaging. Constant-luminance and chromaticity-derived matrices and ICtCp remain incomplete. - The inverse-transform path allocates arrays in a per-transform hot path. -- No usable end-to-end AV1 SIMD path was found. The most visible 4x4 forward-transform SIMD call is commented out, while the production prediction, transform, filter, and output paths are predominantly scalar. +- The production prediction, transform, and nonlinear self-guided paths remain predominantly scalar. Normative super-resolution and Wiener horizontal products now reuse ImageSharp's cross-platform adjacent multiply/add SIMD helper with exact scalar fallbacks; further SIMD work must preserve these scalar reconstruction oracles. ### AV1 encoder diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs b/src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs index cc00a902ef..3409d2dc0a 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs @@ -59,6 +59,7 @@ internal class Av1FrameBuffer : IDisposable /// Indicates whether reconstruction uses native 16-bit sample storage. public Av1FrameBuffer(Configuration configuration, ObuSequenceHeader sequenceHeader, Av1ColorFormat maxColorFormat, bool is16BitPipeline) { + this.MemoryAllocator = configuration.MemoryAllocator; Av1ColorFormat colorFormat = sequenceHeader.ColorConfig.IsMonochrome ? Av1ColorFormat.Yuv400 : maxColorFormat; this.MaxWidth = sequenceHeader.MaxFrameWidth; this.MaxHeight = sequenceHeader.MaxFrameHeight; @@ -198,6 +199,11 @@ internal class Av1FrameBuffer : IDisposable /// public bool Is16BitPipeline { get; } + /// + /// Gets the allocator used for frame-owned and frame-scoped working buffers. + /// + public MemoryAllocator MemoryAllocator { get; } + /// /// Releases the owned luma and chroma plane allocations. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs index d5e2b62e2b..3eac8f267a 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs @@ -4,6 +4,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantification; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.SuperResolution; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; @@ -86,8 +87,7 @@ internal class Av1FrameDecoder : IAv1FrameDecoder this.DecodeFrameTiles(column); } - bool doLoopRestoration = false; - bool doUpscale = this.frameHeader.FrameSize.FrameWidth != this.frameHeader.FrameSize.SuperResolutionUpscaledWidth; + bool doLoopRestoration = this.frameHeader.LoopRestorationParameters.UsesLoopRestoration; Av1LoopFilterDecoder loopFilterDecoder = new( this.sequenceHeader, @@ -98,9 +98,13 @@ internal class Av1FrameDecoder : IAv1FrameDecoder loopFilterDecoder.DecodeFrame(); - if (doLoopRestoration) + using Av1LoopRestorationBoundary? restorationBoundary = doLoopRestoration + ? new(this.sequenceHeader, this.frameHeader, this.frameBuffer) + : null; + + if (restorationBoundary is not null) { - // LoopRestorationSaveBoundaryLines(false); + restorationBoundary.SaveDeblockedRows(); } Av1CdefDecoder cdefDecoder = new(this.sequenceHeader, this.frameHeader, this.frameInfo, this.frameBuffer); @@ -109,13 +113,21 @@ internal class Av1FrameDecoder : IAv1FrameDecoder Av1SuperResolutionDecoder superResolutionDecoder = new(this.sequenceHeader, this.frameHeader, this.frameBuffer); superResolutionDecoder.DecodeFrame(); - if (doLoopRestoration && doUpscale) + if (restorationBoundary is not null) { - // LoopRestorationSaveBoundaryLines(true); + restorationBoundary.SaveFrameEdgeRows(); + Av1LoopRestorationDecoder loopRestorationDecoder = new( + this.sequenceHeader, + this.frameHeader, + this.frameInfo, + this.frameBuffer, + restorationBoundary); + + loopRestorationDecoder.DecodeFrame(); } - // DecodeLoopRestoration(doLoopRestoration); - // PadPicture(); + // A still-image decode ends at the visible restored samples. Extending reference-frame + // borders is sequence playback state and is deliberately outside this decoder's scope. } /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationBoundary.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationBoundary.cs new file mode 100644 index 0000000000..211f886b1e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationBoundary.cs @@ -0,0 +1,438 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.SuperResolution; + +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 +{ + /// + /// The height of a complete restoration processing stripe in luma samples. + /// + public const int ProcessingStripeSize = 64; + + /// + /// The upward offset of the restoration stripe grid from the restoration-unit grid. + /// + public const int ProcessingStripeOffset = 8; + + /// + /// The number of distinct deblocked rows preserved above and below a stripe. + /// + private const int ContextRowCount = 2; + + /// + /// The sequence-level bit-depth and plane-layout configuration. + /// + private readonly ObuSequenceHeader sequenceHeader; + + /// + /// The coded, reconstructed, and upscaled frame dimensions. + /// + private readonly ObuFrameHeader frameHeader; + + /// + /// The reconstructed sample planes read at each pipeline boundary. + /// + private readonly Av1FrameBuffer frameBuffer; + + /// + /// The two preserved rows above every processing stripe, stored by plane. + /// + private readonly IMemoryOwner?[] rowsAbove = new IMemoryOwner?[Av1Constants.MaxPlanes]; + + /// + /// The two preserved rows below every processing stripe, stored by plane. + /// + private readonly IMemoryOwner?[] rowsBelow = new IMemoryOwner?[Av1Constants.MaxPlanes]; + + /// + /// The upscaled sample width stored for each plane boundary row. + /// + private readonly int[] planeWidths = new int[Av1Constants.MaxPlanes]; + + /// + /// The number of processing stripes represented for each plane. + /// + private readonly int[] stripeCounts = new int[Av1Constants.MaxPlanes]; + + /// + /// Initializes a new instance of the class. + /// + /// The sequence header defining bit depth and chroma subsampling. + /// The frame header defining coded and upscaled dimensions. + /// The reconstructed frame sampled before and after CDEF. + public Av1LoopRestorationBoundary( + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1FrameBuffer frameBuffer) + { + this.sequenceHeader = sequenceHeader; + this.frameHeader = frameHeader; + this.frameBuffer = frameBuffer; + + try + { + ObuColorConfig colorConfig = sequenceHeader.ColorConfig; + for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++) + { + if (frameHeader.LoopRestorationParameters.Items[planeIndex].Type == ObuRestorationType.None) + { + continue; + } + + Av1Plane plane = (Av1Plane)planeIndex; + int subsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0; + int subsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0; + int planeWidth = Av1Math.DivideLog2Ceiling( + frameHeader.FrameSize.SuperResolutionUpscaledWidth, + subsamplingX); + + int planeHeight = Av1Math.DivideLog2Ceiling(frameHeader.FrameSize.FrameHeight, subsamplingY); + int stripeHeight = ProcessingStripeSize >> subsamplingY; + int stripeOffset = ProcessingStripeOffset >> subsamplingY; + + // The stripe grid begins eight luma rows above the restoration-unit grid. Including + // that offset in the ceiling retains the short final stripe when one is present. + int stripeCount = (planeHeight + stripeOffset + stripeHeight - 1) / stripeHeight; + int storageLength = stripeCount * ContextRowCount * planeWidth; + this.planeWidths[planeIndex] = planeWidth; + this.stripeCounts[planeIndex] = stripeCount; + this.rowsAbove[planeIndex] = frameBuffer.MemoryAllocator.Allocate(storageLength); + this.rowsBelow[planeIndex] = frameBuffer.MemoryAllocator.Allocate(storageLength); + } + } + catch + { + // The object is not available to its caller when construction fails, so release any + // plane owners acquired before the allocator reported the failure. + this.Dispose(); + throw; + } + } + + /// + /// Releases the preserved plane-boundary storage. + /// + public void Dispose() + { + for (int plane = 0; plane < Av1Constants.MaxPlanes; plane++) + { + this.rowsAbove[plane]?.Dispose(); + this.rowsAbove[plane] = null; + this.rowsBelow[plane]?.Dispose(); + this.rowsBelow[plane] = null; + } + } + + /// + /// Preserves deblocked rows at every internal restoration-stripe boundary. + /// + public void SaveDeblockedRows() + { + ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig; + ObuFrameSize frameSize = this.frameHeader.FrameSize; + bool usesSuperResolution = frameSize.FrameWidth != frameSize.SuperResolutionUpscaledWidth; + for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++) + { + if (this.stripeCounts[planeIndex] == 0) + { + continue; + } + + Av1Plane plane = (Av1Plane)planeIndex; + int subsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0; + int subsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0; + int codedWidth = Av1Math.DivideLog2Ceiling(frameSize.FrameWidth, subsamplingX); + int upscaledWidth = this.planeWidths[planeIndex]; + int reconstructedWidth = this.frameHeader.ModeInfoColumnCount + << (Av1Constants.ModeInfoSizeLog2 - subsamplingX); + + // Super-resolution phase uses the coded width, while its filter taps can consume the + // complete mode-info-aligned reconstruction at the right edge. + int planeHeight = Av1Math.DivideLog2Ceiling(frameSize.FrameHeight, subsamplingY); + int stripeHeight = ProcessingStripeSize >> subsamplingY; + int stripeOffset = ProcessingStripeOffset >> subsamplingY; + int upscaleSourceLength = usesSuperResolution + ? reconstructedWidth + (Av1SuperResolutionKernels.SourceBorder * 2) + : 0; + + using IMemoryOwner? upscaleSourceOwner = usesSuperResolution + ? this.frameBuffer.MemoryAllocator.Allocate(upscaleSourceLength) + : null; + + Span upscaleSource = upscaleSourceOwner is not null + ? upscaleSourceOwner.Memory.Span[..upscaleSourceLength] + : Span.Empty; + + Span lowBitDepthPlane = default; + Span highBitDepthPlane = default; + int sourceStride; + if (this.frameBuffer.BytesPerSample == 2) + { + 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); + } + + int step = usesSuperResolution + ? Av1SuperResolutionKernels.GetConvolveStep(codedWidth, upscaledWidth) + : 0; + + int initialSubpixel = usesSuperResolution + ? Av1SuperResolutionKernels.GetInitialSubpixel(codedWidth, upscaledWidth, step) + : 0; + + for (int stripe = 0; stripe < this.stripeCounts[planeIndex]; stripe++) + { + int stripeStart = Math.Max(0, (stripe * stripeHeight) - stripeOffset); + int stripeEnd = Math.Min(((stripe + 1) * stripeHeight) - stripeOffset, planeHeight); + if (stripe > 0) + { + // Internal top context is the two deblocked rows immediately preceding the + // stripe; restoration later expands the first row to fill its three-row border. + this.SaveDeblockedRow( + lowBitDepthPlane, + highBitDepthPlane, + sourceStride, + stripeStart - ContextRowCount, + reconstructedWidth, + step, + initialSubpixel, + upscaleSource, + this.GetBoundaryRow(this.rowsAbove, planeIndex, stripe, 0)); + + this.SaveDeblockedRow( + lowBitDepthPlane, + highBitDepthPlane, + sourceStride, + stripeStart - 1, + reconstructedWidth, + step, + initialSubpixel, + upscaleSource, + this.GetBoundaryRow(this.rowsAbove, planeIndex, stripe, 1)); + } + + if (stripeEnd < planeHeight) + { + // Internal bottom context begins at the exclusive stripe end. A one-row tail + // duplicates its final sample row, matching AV1 crop-edge clamping. + this.SaveDeblockedRow( + lowBitDepthPlane, + highBitDepthPlane, + sourceStride, + stripeEnd, + reconstructedWidth, + step, + initialSubpixel, + upscaleSource, + this.GetBoundaryRow(this.rowsBelow, planeIndex, stripe, 0)); + + this.SaveDeblockedRow( + lowBitDepthPlane, + highBitDepthPlane, + sourceStride, + Math.Min(stripeEnd + 1, planeHeight - 1), + reconstructedWidth, + step, + initialSubpixel, + upscaleSource, + this.GetBoundaryRow(this.rowsBelow, planeIndex, stripe, 1)); + } + } + } + } + + /// + /// Preserves the post-CDEF and post-super-resolution rows at the top and bottom of each active plane. + /// + public void SaveFrameEdgeRows() + { + ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig; + ObuFrameSize frameSize = this.frameHeader.FrameSize; + for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++) + { + int stripeCount = this.stripeCounts[planeIndex]; + if (stripeCount == 0) + { + continue; + } + + Av1Plane plane = (Av1Plane)planeIndex; + int subsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0; + int subsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0; + int planeHeight = Av1Math.DivideLog2Ceiling(frameSize.FrameHeight, subsamplingY); + Span lowBitDepthPlane = default; + Span highBitDepthPlane = default; + int sourceStride; + if (this.frameBuffer.BytesPerSample == 2) + { + 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); + } + + Span topRow0 = this.GetBoundaryRow(this.rowsAbove, planeIndex, 0, 0); + Span topRow1 = this.GetBoundaryRow(this.rowsAbove, planeIndex, 0, 1); + CopyFrameRow(lowBitDepthPlane, highBitDepthPlane, sourceStride, 0, topRow0); + + // Frame boundaries use post-CDEF/post-super-resolution samples and replicate the outer row. + topRow0.CopyTo(topRow1); + + int lastStripe = stripeCount - 1; + Span bottomRow0 = this.GetBoundaryRow(this.rowsBelow, planeIndex, lastStripe, 0); + Span bottomRow1 = this.GetBoundaryRow(this.rowsBelow, planeIndex, lastStripe, 1); + CopyFrameRow(lowBitDepthPlane, highBitDepthPlane, sourceStride, planeHeight - 1, bottomRow0); + bottomRow0.CopyTo(bottomRow1); + } + } + + /// + /// Gets one preserved row above a restoration processing stripe. + /// + /// The zero-based color-plane index. + /// The frame-relative processing-stripe index. + /// The first or second preserved context row. + /// The preserved upscaled row. + public ReadOnlySpan GetRowAbove(int plane, int stripe, int contextRow) + => this.GetBoundaryRow(this.rowsAbove, plane, stripe, contextRow); + + /// + /// Gets one preserved row below a restoration processing stripe. + /// + /// The zero-based color-plane index. + /// The frame-relative processing-stripe index. + /// The first or second preserved context row. + /// The preserved upscaled row. + public ReadOnlySpan GetRowBelow(int plane, int stripe, int contextRow) + => this.GetBoundaryRow(this.rowsBelow, plane, stripe, contextRow); + + /// + /// Preserves one deblocked row, applying normative super-resolution when the frame is scaled. + /// + /// The byte-backed plane when the frame uses eight-bit samples. + /// The native 16-bit plane when the frame uses high-bit-depth samples. + /// The number of samples between reconstructed rows. + /// The reconstructed row to preserve. + /// The mode-info-aligned width supplying super-resolution taps. + /// The fixed-point super-resolution source-position increment, or zero when unscaled. + /// The first super-resolution source position, or zero when unscaled. + /// The replicated-edge super-resolution work row. + /// The preserved upscaled boundary row. + private void SaveDeblockedRow( + ReadOnlySpan lowBitDepthPlane, + ReadOnlySpan highBitDepthPlane, + int sourceStride, + int row, + int reconstructedWidth, + int step, + int initialSubpixel, + Span upscaleSource, + Span destination) + { + if (upscaleSource.IsEmpty) + { + CopyFrameRow(lowBitDepthPlane, highBitDepthPlane, sourceStride, row, destination); + return; + } + + int sourceStart = Av1SuperResolutionKernels.SourceBorder; + Span reconstructedSamples = upscaleSource.Slice(sourceStart, reconstructedWidth); + CopyFrameRow(lowBitDepthPlane, highBitDepthPlane, sourceStride, row, reconstructedSamples); + + // Boundary rows must follow the same continuous phase and aligned right-edge behavior as + // the full-frame super-resolution stage or restoration would see different stripe context. + upscaleSource[..sourceStart].Fill(reconstructedSamples[0]); + upscaleSource[(sourceStart + reconstructedWidth)..].Fill(reconstructedSamples[^1]); + Av1SuperResolutionKernels.UpscaleRow( + upscaleSource, + destination, + step, + initialSubpixel, + this.frameBuffer.BitDepth.GetBitCount()); + } + + /// + /// Copies one reconstructed sample row into 16-bit working storage. + /// + /// The byte-backed plane when the frame uses eight-bit samples. + /// The native 16-bit plane when the frame uses high-bit-depth samples. + /// The number of samples between reconstructed rows. + /// The zero-based visible row index. + /// The destination row whose length determines the copied width. + private static void CopyFrameRow( + ReadOnlySpan lowBitDepthPlane, + ReadOnlySpan highBitDepthPlane, + int sourceStride, + int row, + Span destination) + { + int sourceOffset = sourceStride + (row * sourceStride); + if (!highBitDepthPlane.IsEmpty) + { + highBitDepthPlane.Slice(sourceOffset, destination.Length).CopyTo(destination); + return; + } + + ReadOnlySpan source = lowBitDepthPlane.Slice(sourceOffset, destination.Length); + for (int column = 0; column < destination.Length; column++) + { + destination[column] = source[column]; + } + } + + /// + /// Gets writable storage for one plane-relative boundary row. + /// + /// The above- or below-stripe storage for every plane. + /// The zero-based color-plane index. + /// The frame-relative processing-stripe index. + /// The first or second preserved context row. + /// The selected boundary-row span. + private Span GetBoundaryRow( + IMemoryOwner?[] 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); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationDecoder.cs new file mode 100644 index 0000000000..9fa00f9ad9 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationDecoder.cs @@ -0,0 +1,562 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration; + +/// +/// Applies decoded AV1 loop-restoration units to a reconstructed still-image frame. +/// +internal class Av1LoopRestorationDecoder +{ + /// + /// The number of source rows and columns required around each filtered processing stripe. + /// + private const int FilterBorder = 3; + + /// + /// The additional zero-coefficient tap read by the padded Wiener convolution kernel. + /// + private const int WienerPadding = 1; + + /// + /// The sequence-level bit-depth and plane-layout configuration. + /// + private readonly ObuSequenceHeader sequenceHeader; + + /// + /// The frame-level restoration-unit and dimension configuration. + /// + private readonly ObuFrameHeader frameHeader; + + /// + /// The decoded restoration filter and coefficient selections. + /// + private readonly Av1FrameInfo frameInfo; + + /// + /// The reconstructed sample planes updated with restored output. + /// + private readonly Av1FrameBuffer frameBuffer; + + /// + /// The preserved deblocked rows used at restoration-stripe boundaries. + /// + private readonly Av1LoopRestorationBoundary boundary; + + /// + /// Initializes a new instance of the class. + /// + /// The sequence header defining bit depth and chroma subsampling. + /// The frame header defining restoration-unit sizes and frame dimensions. + /// The decoded restoration-unit selections and coefficients. + /// The CDEF-filtered and upscaled frame samples. + /// The deblocked context preserved around restoration stripes. + public Av1LoopRestorationDecoder( + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1FrameInfo frameInfo, + Av1FrameBuffer frameBuffer, + Av1LoopRestorationBoundary boundary) + { + this.sequenceHeader = sequenceHeader; + this.frameHeader = frameHeader; + this.frameInfo = frameInfo; + this.frameBuffer = frameBuffer; + this.boundary = boundary; + } + + /// + /// Restores every active color plane from an immutable post-super-resolution source snapshot. + /// + public void DecodeFrame() + { + ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig; + for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++) + { + ObuLoopRestorationItem item = this.frameHeader.LoopRestorationParameters.Items[planeIndex]; + if (item.Type == ObuRestorationType.None) + { + continue; + } + + Av1Plane plane = (Av1Plane)planeIndex; + int subsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0; + int subsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0; + this.DecodePlane(plane, subsamplingX, subsamplingY, item.Size); + } + } + + /// + /// Restores one color plane in raster-ordered restoration units. + /// + /// The luma or chroma plane. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// The nominal restoration-unit width and height in plane samples. + private void DecodePlane(Av1Plane plane, int subsamplingX, int subsamplingY, int unitSize) + { + int planeIndex = (int)plane; + ObuFrameSize frameSize = this.frameHeader.FrameSize; + int planeWidth = Av1Math.DivideLog2Ceiling(frameSize.SuperResolutionUpscaledWidth, subsamplingX); + 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]; + + // 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); + + // 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. + int extendedUnitSize = (unitSize * 3) / 2; + int maximumUnitWidth = Math.Min(extendedUnitSize, planeWidth); + int maximumStripeHeight = Av1LoopRestorationBoundary.ProcessingStripeSize >> subsamplingY; + 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 processingUnitWidth = Av1LoopRestorationBoundary.ProcessingStripeSize >> subsamplingX; + int maximumSelfGuidedWidth = Math.Min(processingUnitWidth, maximumUnitWidth); + int selfGuidedScratchLength = Av1SelfGuidedFilter.GetScratchLength(maximumSelfGuidedWidth, maximumStripeHeight); + using IMemoryOwner selfGuidedScratchOwner = allocator.Allocate(selfGuidedScratchLength); + Span selfGuidedScratch = selfGuidedScratchOwner.Memory.Span[..selfGuidedScratchLength]; + + int unitRowCount = this.frameInfo.GetLoopRestorationUnitRowCount(planeIndex); + int unitColumnCount = this.frameInfo.GetLoopRestorationUnitColumnCount(planeIndex); + int unitY = 0; + for (int unitRow = 0; unitRow < unitRowCount; unitRow++) + { + int remainingHeight = planeHeight - unitY; + int unadjustedUnitHeight = remainingHeight < extendedUnitSize ? remainingHeight : unitSize; + int verticalStart = unitY; + int verticalEnd = unitY + unadjustedUnitHeight; + int verticalOffset = Av1LoopRestorationBoundary.ProcessingStripeOffset >> subsamplingY; + + // Unit ownership is signaled on the unshifted grid, but filtering rows follow the + // processing-stripe grid positioned eight luma samples above it. + verticalStart = Math.Max(0, verticalStart - verticalOffset); + if (verticalEnd < planeHeight) + { + verticalEnd -= verticalOffset; + } + + int unitX = 0; + for (int unitColumn = 0; unitColumn < unitColumnCount; unitColumn++) + { + int remainingWidth = planeWidth - unitX; + int unitWidth = remainingWidth < extendedUnitSize ? remainingWidth : unitSize; + Av1LoopRestorationUnit unit = this.frameInfo.GetLoopRestorationUnit( + planeIndex, + unitRow, + unitColumn); + + this.FilterUnit( + planeIndex, + subsamplingX, + source, + destination, + planeWidth, + planeHeight, + unitX, + unitX + unitWidth, + verticalStart, + verticalEnd, + unit, + borderedSource, + wienerScratch, + selfGuidedScratch); + + unitX += unitWidth; + } + + unitY += unadjustedUnitHeight; + } + + this.CopyWorkingBufferToPlane(plane, subsamplingX, subsamplingY, planeWidth, planeHeight, destination); + } + + /// + /// Copies or filters one restoration unit without reading already restored destination samples. + /// + /// The zero-based color-plane index. + /// The horizontal chroma subsampling shift. + /// The immutable post-super-resolution plane samples. + /// The restored destination plane samples. + /// The visible plane width. + /// The visible plane height. + /// The unit's first plane column. + /// The exclusive unit column limit. + /// The unit's stripe-aligned first plane row. + /// The exclusive unit row limit. + /// The decoded unit filter and coefficients. + /// Reusable storage for one bordered processing stripe. + /// Reusable Wiener intermediate storage. + /// Reusable self-guided intermediate storage. + private void FilterUnit( + int plane, + int subsamplingX, + ReadOnlySpan source, + Span destination, + int planeWidth, + int planeHeight, + int horizontalStart, + int horizontalEnd, + int verticalStart, + int verticalEnd, + Av1LoopRestorationUnit unit, + Span borderedSource, + Span wienerScratch, + Span selfGuidedScratch) + { + int unitWidth = horizontalEnd - horizontalStart; + if (unit.FilterType == Av1RestorationFilterType.None) + { + // 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, + planeWidth, + horizontalStart, + unitWidth, + verticalStart, + verticalEnd); + + return; + } + + int subsamplingY = plane == (int)Av1Plane.Y || !this.sequenceHeader.ColorConfig.SubSamplingY ? 0 : 1; + int fullStripeHeight = Av1LoopRestorationBoundary.ProcessingStripeSize >> subsamplingY; + int stripeOffset = Av1LoopRestorationBoundary.ProcessingStripeOffset >> subsamplingY; + int unitHeight = verticalEnd - verticalStart; + for (int unitRow = 0; unitRow < unitHeight;) + { + int stripeStart = verticalStart + unitRow; + int frameStripe = (stripeStart + stripeOffset) / fullStripeHeight; + int nominalStripeHeight = fullStripeHeight - (frameStripe == 0 ? stripeOffset : 0); + int stripeHeight = Math.Min(nominalStripeHeight, verticalEnd - stripeStart); + + // The first frame stripe is shortened by the upward offset; subsequent stripes remain + // 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); + Span filterSource = borderedSource[..sourceLength]; + this.PopulateBorderedSource( + plane, + frameStripe, + source, + planeWidth, + planeHeight, + horizontalStart, + unitWidth, + stripeStart, + stripeHeight, + sourceStride, + filterSource); + + int destinationOffset = (stripeStart * planeWidth) + horizontalStart; + int scratchLength = Av1WienerFilter.GetScratchLength(unitWidth, stripeHeight); + Av1WienerFilter.FilterStripe( + filterSource, + sourceStride, + destination[destinationOffset..], + planeWidth, + unitWidth, + stripeHeight, + this.frameBuffer.BitDepth.GetBitCount(), + unit.WienerHorizontal, + unit.WienerVertical, + wienerScratch[..scratchLength]); + } + else + { + int processingUnitWidth = Av1LoopRestorationBoundary.ProcessingStripeSize >> subsamplingX; + + // Self-guided local statistics restart at each normative 64-luma processing unit. + // Context still crosses the chunk boundary because the source is the full plane. + for (int unitColumn = 0; unitColumn < unitWidth; unitColumn += processingUnitWidth) + { + int blockWidth = Math.Min(processingUnitWidth, unitWidth - unitColumn); + int blockStart = horizontalStart + unitColumn; + int sourceStride = blockWidth + (FilterBorder * 2) + WienerPadding; + int sourceLength = sourceStride * (stripeHeight + (FilterBorder * 2) + WienerPadding); + Span filterSource = borderedSource[..sourceLength]; + this.PopulateBorderedSource( + plane, + frameStripe, + source, + planeWidth, + planeHeight, + blockStart, + blockWidth, + stripeStart, + stripeHeight, + sourceStride, + filterSource); + + int destinationOffset = (stripeStart * planeWidth) + blockStart; + int scratchLength = Av1SelfGuidedFilter.GetScratchLength(blockWidth, stripeHeight); + Av1SelfGuidedFilter.FilterBlock( + filterSource, + sourceStride, + destination[destinationOffset..], + planeWidth, + blockWidth, + stripeHeight, + this.frameBuffer.BitDepth.GetBitCount(), + unit.SgrParameterSet, + unit.SgrProjectionCoefficients, + selfGuidedScratch[..scratchLength]); + } + } + + unitRow += stripeHeight; + } + } + + /// + /// Builds one filter source rectangle with normative horizontal and stripe-boundary extension. + /// + /// The zero-based color-plane index. + /// The frame-relative processing-stripe index. + /// The immutable post-super-resolution plane samples. + /// The visible plane width. + /// The visible plane height. + /// The first filtered plane column. + /// The number of filtered columns. + /// The first filtered plane row. + /// The number of filtered rows. + /// The number of samples between bordered destination rows. + /// The bordered filter source rectangle. + private void PopulateBorderedSource( + int plane, + int frameStripe, + ReadOnlySpan source, + int planeWidth, + int planeHeight, + int blockStart, + int blockWidth, + int stripeStart, + int stripeHeight, + int destinationStride, + Span destination) + { + int stripeEnd = stripeStart + stripeHeight; + int sourceRowCount = stripeHeight + (FilterBorder * 2) + WienerPadding; + for (int destinationRow = 0; destinationRow < sourceRowCount; destinationRow++) + { + int sourceY = stripeStart + destinationRow - FilterBorder; + ReadOnlySpan sourceRow; + if (sourceY < 0) + { + sourceRow = 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); + } + else if (sourceY >= planeHeight) + { + sourceRow = 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); + } + else + { + sourceRow = source.Slice(sourceY * planeWidth, planeWidth); + } + + Span destinationRowSpan = destination.Slice(destinationRow * destinationStride, destinationStride); + int sourceX = blockStart - FilterBorder; + int leftExtension = Math.Max(-sourceX, 0); + + // Horizontal context crosses restoration-unit and self-guided processing-unit edges. + // Replication occurs only at the visible frame boundary. + if (leftExtension > 0) + { + destinationRowSpan[..leftExtension].Fill(sourceRow[0]); + } + + int copiedStart = Math.Max(sourceX, 0); + int copiedEnd = Math.Min(sourceX + destinationStride, planeWidth); + int copiedLength = copiedEnd - copiedStart; + sourceRow.Slice(copiedStart, copiedLength).CopyTo(destinationRowSpan[leftExtension..]); + + int populatedLength = leftExtension + copiedLength; + if (populatedLength < destinationStride) + { + destinationRowSpan[populatedLength..].Fill(sourceRow[^1]); + } + } + } + + /// + /// Copies an unfiltered restoration-unit rectangle between plane working buffers. + /// + /// The immutable source plane. + /// The destination plane. + /// 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, + int planeWidth, + int horizontalStart, + int width, + int verticalStart, + int verticalEnd) + { + for (int row = verticalStart; row < verticalEnd; row++) + { + int offset = (row * planeWidth) + horizontalStart; + source.Slice(offset, width).CopyTo(destination[offset..]); + } + } + + /// + /// Copies one reconstructed plane into an immutable 16-bit working buffer. + /// + /// 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) + { + Span lowBitDepthPlane = default; + Span highBitDepthPlane = default; + int sourceStride; + if (this.frameBuffer.BytesPerSample == 2) + { + 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); + } + + 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 + { + ReadOnlySpan sourceRow = lowBitDepthPlane.Slice(frameOffset, planeWidth); + for (int column = 0; column < planeWidth; column++) + { + destinationRow[column] = sourceRow[column]; + } + } + } + } + + /// + /// Copies one restored 16-bit working 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. + private void CopyWorkingBufferToPlane( + Av1Plane plane, + int subsamplingX, + int subsamplingY, + int planeWidth, + int planeHeight, + ReadOnlySpan source) + { + Span lowBitDepthPlane = default; + Span highBitDepthPlane = default; + int destinationStride; + if (this.frameBuffer.BytesPerSample == 2) + { + Span signedPlane = this.frameBuffer.DeriveBlockPointer16( + plane, + Point.Empty, + subsamplingX, + subsamplingY, + out destinationStride); + + highBitDepthPlane = MemoryMarshal.Cast(signedPlane); + } + else + { + lowBitDepthPlane = this.frameBuffer.DeriveBlockPointer( + plane, + Point.Empty, + subsamplingX, + subsamplingY, + out destinationStride); + } + + for (int row = 0; row < planeHeight; row++) + { + // 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..]); + } + else + { + Span destinationRow = lowBitDepthPlane.Slice(frameOffset, planeWidth); + for (int column = 0; column < planeWidth; column++) + { + destinationRow[column] = (byte)sourceRow[column]; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs index af0a452c3a..92744b1877 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs @@ -115,6 +115,11 @@ internal partial class Av1FrameInfo /// private readonly int[] loopRestorationUnitColumns = new int[Av1Constants.MaxPlanes]; + /// + /// The number of loop-restoration unit rows allocated for each color plane. + /// + private readonly int[] loopRestorationUnitRows = new int[Av1Constants.MaxPlanes]; + /// /// Initializes a new instance of the class. /// @@ -216,6 +221,7 @@ internal partial class Av1FrameInfo } this.loopRestorationUnitColumns[planeIndex] = columnCount; + this.loopRestorationUnitRows[planeIndex] = rowCount; this.loopRestorationUnits[planeIndex] = units; } } @@ -407,6 +413,13 @@ internal partial class Av1FrameInfo /// The number of restoration-unit columns. public int GetLoopRestorationUnitColumnCount(int plane) => this.loopRestorationUnitColumns[plane]; + /// + /// Gets the number of loop-restoration unit rows allocated for a color plane. + /// + /// The zero-based color-plane index. + /// The number of restoration-unit rows. + public int GetLoopRestorationUnitRowCount(int plane) => this.loopRestorationUnitRows[plane]; + /// /// Gets the loop-restoration unit at a plane-relative grid position. ///