mirror of https://github.com/SixLabors/ImageSharp
6 changed files with 1043 additions and 11 deletions
@ -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; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Preserves the deblocked and frame-edge rows required by AV1 striped loop restoration.
|
||||
|
/// </summary>
|
||||
|
internal class Av1LoopRestorationBoundary : IDisposable |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The height of a complete restoration processing stripe in luma samples.
|
||||
|
/// </summary>
|
||||
|
public const int ProcessingStripeSize = 64; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The upward offset of the restoration stripe grid from the restoration-unit grid.
|
||||
|
/// </summary>
|
||||
|
public const int ProcessingStripeOffset = 8; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The number of distinct deblocked rows preserved above and below a stripe.
|
||||
|
/// </summary>
|
||||
|
private const int ContextRowCount = 2; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The sequence-level bit-depth and plane-layout configuration.
|
||||
|
/// </summary>
|
||||
|
private readonly ObuSequenceHeader sequenceHeader; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The coded, reconstructed, and upscaled frame dimensions.
|
||||
|
/// </summary>
|
||||
|
private readonly ObuFrameHeader frameHeader; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The reconstructed sample planes read at each pipeline boundary.
|
||||
|
/// </summary>
|
||||
|
private readonly Av1FrameBuffer<byte> frameBuffer; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The two preserved rows above every processing stripe, stored by plane.
|
||||
|
/// </summary>
|
||||
|
private readonly IMemoryOwner<ushort>?[] rowsAbove = new IMemoryOwner<ushort>?[Av1Constants.MaxPlanes]; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The two preserved rows below every processing stripe, stored by plane.
|
||||
|
/// </summary>
|
||||
|
private readonly IMemoryOwner<ushort>?[] rowsBelow = new IMemoryOwner<ushort>?[Av1Constants.MaxPlanes]; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The upscaled sample width stored for each plane boundary row.
|
||||
|
/// </summary>
|
||||
|
private readonly int[] planeWidths = new int[Av1Constants.MaxPlanes]; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The number of processing stripes represented for each plane.
|
||||
|
/// </summary>
|
||||
|
private readonly int[] stripeCounts = new int[Av1Constants.MaxPlanes]; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="Av1LoopRestorationBoundary"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="sequenceHeader">The sequence header defining bit depth and chroma subsampling.</param>
|
||||
|
/// <param name="frameHeader">The frame header defining coded and upscaled dimensions.</param>
|
||||
|
/// <param name="frameBuffer">The reconstructed frame sampled before and after CDEF.</param>
|
||||
|
public Av1LoopRestorationBoundary( |
||||
|
ObuSequenceHeader sequenceHeader, |
||||
|
ObuFrameHeader frameHeader, |
||||
|
Av1FrameBuffer<byte> 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<ushort>(storageLength); |
||||
|
this.rowsBelow[planeIndex] = frameBuffer.MemoryAllocator.Allocate<ushort>(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; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Releases the preserved plane-boundary storage.
|
||||
|
/// </summary>
|
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Preserves deblocked rows at every internal restoration-stripe boundary.
|
||||
|
/// </summary>
|
||||
|
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<ushort>? upscaleSourceOwner = usesSuperResolution |
||||
|
? this.frameBuffer.MemoryAllocator.Allocate<ushort>(upscaleSourceLength) |
||||
|
: null; |
||||
|
|
||||
|
Span<ushort> upscaleSource = upscaleSourceOwner is not null |
||||
|
? upscaleSourceOwner.Memory.Span[..upscaleSourceLength] |
||||
|
: Span<ushort>.Empty; |
||||
|
|
||||
|
Span<byte> lowBitDepthPlane = default; |
||||
|
Span<ushort> highBitDepthPlane = default; |
||||
|
int sourceStride; |
||||
|
if (this.frameBuffer.BytesPerSample == 2) |
||||
|
{ |
||||
|
Span<short> signedPlane = this.frameBuffer.DeriveBlockPointer16( |
||||
|
plane, |
||||
|
Point.Empty, |
||||
|
subsamplingX, |
||||
|
subsamplingY, |
||||
|
out sourceStride); |
||||
|
|
||||
|
highBitDepthPlane = MemoryMarshal.Cast<short, ushort>(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)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Preserves the post-CDEF and post-super-resolution rows at the top and bottom of each active plane.
|
||||
|
/// </summary>
|
||||
|
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<byte> lowBitDepthPlane = default; |
||||
|
Span<ushort> highBitDepthPlane = default; |
||||
|
int sourceStride; |
||||
|
if (this.frameBuffer.BytesPerSample == 2) |
||||
|
{ |
||||
|
Span<short> signedPlane = this.frameBuffer.DeriveBlockPointer16( |
||||
|
plane, |
||||
|
Point.Empty, |
||||
|
subsamplingX, |
||||
|
subsamplingY, |
||||
|
out sourceStride); |
||||
|
|
||||
|
highBitDepthPlane = MemoryMarshal.Cast<short, ushort>(signedPlane); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
lowBitDepthPlane = this.frameBuffer.DeriveBlockPointer( |
||||
|
plane, |
||||
|
Point.Empty, |
||||
|
subsamplingX, |
||||
|
subsamplingY, |
||||
|
out sourceStride); |
||||
|
} |
||||
|
|
||||
|
Span<ushort> topRow0 = this.GetBoundaryRow(this.rowsAbove, planeIndex, 0, 0); |
||||
|
Span<ushort> 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<ushort> bottomRow0 = this.GetBoundaryRow(this.rowsBelow, planeIndex, lastStripe, 0); |
||||
|
Span<ushort> bottomRow1 = this.GetBoundaryRow(this.rowsBelow, planeIndex, lastStripe, 1); |
||||
|
CopyFrameRow(lowBitDepthPlane, highBitDepthPlane, sourceStride, planeHeight - 1, bottomRow0); |
||||
|
bottomRow0.CopyTo(bottomRow1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets one preserved row above a restoration processing stripe.
|
||||
|
/// </summary>
|
||||
|
/// <param name="plane">The zero-based color-plane index.</param>
|
||||
|
/// <param name="stripe">The frame-relative processing-stripe index.</param>
|
||||
|
/// <param name="contextRow">The first or second preserved context row.</param>
|
||||
|
/// <returns>The preserved upscaled row.</returns>
|
||||
|
public ReadOnlySpan<ushort> GetRowAbove(int plane, int stripe, int contextRow) |
||||
|
=> this.GetBoundaryRow(this.rowsAbove, plane, stripe, contextRow); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets one preserved row below a restoration processing stripe.
|
||||
|
/// </summary>
|
||||
|
/// <param name="plane">The zero-based color-plane index.</param>
|
||||
|
/// <param name="stripe">The frame-relative processing-stripe index.</param>
|
||||
|
/// <param name="contextRow">The first or second preserved context row.</param>
|
||||
|
/// <returns>The preserved upscaled row.</returns>
|
||||
|
public ReadOnlySpan<ushort> GetRowBelow(int plane, int stripe, int contextRow) |
||||
|
=> this.GetBoundaryRow(this.rowsBelow, plane, stripe, contextRow); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Preserves one deblocked row, applying normative super-resolution when the frame is scaled.
|
||||
|
/// </summary>
|
||||
|
/// <param name="lowBitDepthPlane">The byte-backed plane when the frame uses eight-bit samples.</param>
|
||||
|
/// <param name="highBitDepthPlane">The native 16-bit plane when the frame uses high-bit-depth samples.</param>
|
||||
|
/// <param name="sourceStride">The number of samples between reconstructed rows.</param>
|
||||
|
/// <param name="row">The reconstructed row to preserve.</param>
|
||||
|
/// <param name="reconstructedWidth">The mode-info-aligned width supplying super-resolution taps.</param>
|
||||
|
/// <param name="step">The fixed-point super-resolution source-position increment, or zero when unscaled.</param>
|
||||
|
/// <param name="initialSubpixel">The first super-resolution source position, or zero when unscaled.</param>
|
||||
|
/// <param name="upscaleSource">The replicated-edge super-resolution work row.</param>
|
||||
|
/// <param name="destination">The preserved upscaled boundary row.</param>
|
||||
|
private void SaveDeblockedRow( |
||||
|
ReadOnlySpan<byte> lowBitDepthPlane, |
||||
|
ReadOnlySpan<ushort> highBitDepthPlane, |
||||
|
int sourceStride, |
||||
|
int row, |
||||
|
int reconstructedWidth, |
||||
|
int step, |
||||
|
int initialSubpixel, |
||||
|
Span<ushort> upscaleSource, |
||||
|
Span<ushort> destination) |
||||
|
{ |
||||
|
if (upscaleSource.IsEmpty) |
||||
|
{ |
||||
|
CopyFrameRow(lowBitDepthPlane, highBitDepthPlane, sourceStride, row, destination); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
int sourceStart = Av1SuperResolutionKernels.SourceBorder; |
||||
|
Span<ushort> 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()); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Copies one reconstructed sample row into 16-bit working storage.
|
||||
|
/// </summary>
|
||||
|
/// <param name="lowBitDepthPlane">The byte-backed plane when the frame uses eight-bit samples.</param>
|
||||
|
/// <param name="highBitDepthPlane">The native 16-bit plane when the frame uses high-bit-depth samples.</param>
|
||||
|
/// <param name="sourceStride">The number of samples between reconstructed rows.</param>
|
||||
|
/// <param name="row">The zero-based visible row index.</param>
|
||||
|
/// <param name="destination">The destination row whose length determines the copied width.</param>
|
||||
|
private static void CopyFrameRow( |
||||
|
ReadOnlySpan<byte> lowBitDepthPlane, |
||||
|
ReadOnlySpan<ushort> highBitDepthPlane, |
||||
|
int sourceStride, |
||||
|
int row, |
||||
|
Span<ushort> destination) |
||||
|
{ |
||||
|
int sourceOffset = sourceStride + (row * sourceStride); |
||||
|
if (!highBitDepthPlane.IsEmpty) |
||||
|
{ |
||||
|
highBitDepthPlane.Slice(sourceOffset, destination.Length).CopyTo(destination); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
ReadOnlySpan<byte> source = lowBitDepthPlane.Slice(sourceOffset, destination.Length); |
||||
|
for (int column = 0; column < destination.Length; column++) |
||||
|
{ |
||||
|
destination[column] = source[column]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets writable storage for one plane-relative boundary row.
|
||||
|
/// </summary>
|
||||
|
/// <param name="storage">The above- or below-stripe storage for every plane.</param>
|
||||
|
/// <param name="plane">The zero-based color-plane index.</param>
|
||||
|
/// <param name="stripe">The frame-relative processing-stripe index.</param>
|
||||
|
/// <param name="contextRow">The first or second preserved context row.</param>
|
||||
|
/// <returns>The selected boundary-row span.</returns>
|
||||
|
private Span<ushort> GetBoundaryRow( |
||||
|
IMemoryOwner<ushort>?[] 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); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Applies decoded AV1 loop-restoration units to a reconstructed still-image frame.
|
||||
|
/// </summary>
|
||||
|
internal class Av1LoopRestorationDecoder |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The number of source rows and columns required around each filtered processing stripe.
|
||||
|
/// </summary>
|
||||
|
private const int FilterBorder = 3; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The additional zero-coefficient tap read by the padded Wiener convolution kernel.
|
||||
|
/// </summary>
|
||||
|
private const int WienerPadding = 1; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The sequence-level bit-depth and plane-layout configuration.
|
||||
|
/// </summary>
|
||||
|
private readonly ObuSequenceHeader sequenceHeader; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The frame-level restoration-unit and dimension configuration.
|
||||
|
/// </summary>
|
||||
|
private readonly ObuFrameHeader frameHeader; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The decoded restoration filter and coefficient selections.
|
||||
|
/// </summary>
|
||||
|
private readonly Av1FrameInfo frameInfo; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The reconstructed sample planes updated with restored output.
|
||||
|
/// </summary>
|
||||
|
private readonly Av1FrameBuffer<byte> frameBuffer; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The preserved deblocked rows used at restoration-stripe boundaries.
|
||||
|
/// </summary>
|
||||
|
private readonly Av1LoopRestorationBoundary boundary; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="Av1LoopRestorationDecoder"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="sequenceHeader">The sequence header defining bit depth and chroma subsampling.</param>
|
||||
|
/// <param name="frameHeader">The frame header defining restoration-unit sizes and frame dimensions.</param>
|
||||
|
/// <param name="frameInfo">The decoded restoration-unit selections and coefficients.</param>
|
||||
|
/// <param name="frameBuffer">The CDEF-filtered and upscaled frame samples.</param>
|
||||
|
/// <param name="boundary">The deblocked context preserved around restoration stripes.</param>
|
||||
|
public Av1LoopRestorationDecoder( |
||||
|
ObuSequenceHeader sequenceHeader, |
||||
|
ObuFrameHeader frameHeader, |
||||
|
Av1FrameInfo frameInfo, |
||||
|
Av1FrameBuffer<byte> frameBuffer, |
||||
|
Av1LoopRestorationBoundary boundary) |
||||
|
{ |
||||
|
this.sequenceHeader = sequenceHeader; |
||||
|
this.frameHeader = frameHeader; |
||||
|
this.frameInfo = frameInfo; |
||||
|
this.frameBuffer = frameBuffer; |
||||
|
this.boundary = boundary; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Restores every active color plane from an immutable post-super-resolution source snapshot.
|
||||
|
/// </summary>
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Restores one color plane in raster-ordered restoration units.
|
||||
|
/// </summary>
|
||||
|
/// <param name="plane">The luma or chroma plane.</param>
|
||||
|
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
|
||||
|
/// <param name="subsamplingY">The vertical chroma subsampling shift.</param>
|
||||
|
/// <param name="unitSize">The nominal restoration-unit width and height in plane samples.</param>
|
||||
|
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<ushort> sourceOwner = allocator.Allocate<ushort>(planeLength); |
||||
|
using IMemoryOwner<ushort> destinationOwner = allocator.Allocate<ushort>(planeLength); |
||||
|
Span<ushort> source = sourceOwner.Memory.Span[..planeLength]; |
||||
|
Span<ushort> 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<ushort> borderedSourceOwner = allocator.Allocate<ushort>(borderedLength); |
||||
|
using IMemoryOwner<ushort> wienerScratchOwner = allocator.Allocate<ushort>(wienerScratchLength); |
||||
|
Span<ushort> borderedSource = borderedSourceOwner.Memory.Span[..borderedLength]; |
||||
|
Span<ushort> wienerScratch = wienerScratchOwner.Memory.Span[..wienerScratchLength]; |
||||
|
int processingUnitWidth = Av1LoopRestorationBoundary.ProcessingStripeSize >> subsamplingX; |
||||
|
int maximumSelfGuidedWidth = Math.Min(processingUnitWidth, maximumUnitWidth); |
||||
|
int selfGuidedScratchLength = Av1SelfGuidedFilter.GetScratchLength(maximumSelfGuidedWidth, maximumStripeHeight); |
||||
|
using IMemoryOwner<int> selfGuidedScratchOwner = allocator.Allocate<int>(selfGuidedScratchLength); |
||||
|
Span<int> 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); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Copies or filters one restoration unit without reading already restored destination samples.
|
||||
|
/// </summary>
|
||||
|
/// <param name="plane">The zero-based color-plane index.</param>
|
||||
|
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
|
||||
|
/// <param name="source">The immutable post-super-resolution plane samples.</param>
|
||||
|
/// <param name="destination">The restored destination plane samples.</param>
|
||||
|
/// <param name="planeWidth">The visible plane width.</param>
|
||||
|
/// <param name="planeHeight">The visible plane height.</param>
|
||||
|
/// <param name="horizontalStart">The unit's first plane column.</param>
|
||||
|
/// <param name="horizontalEnd">The exclusive unit column limit.</param>
|
||||
|
/// <param name="verticalStart">The unit's stripe-aligned first plane row.</param>
|
||||
|
/// <param name="verticalEnd">The exclusive unit row limit.</param>
|
||||
|
/// <param name="unit">The decoded unit filter and coefficients.</param>
|
||||
|
/// <param name="borderedSource">Reusable storage for one bordered processing stripe.</param>
|
||||
|
/// <param name="wienerScratch">Reusable Wiener intermediate storage.</param>
|
||||
|
/// <param name="selfGuidedScratch">Reusable self-guided intermediate storage.</param>
|
||||
|
private void FilterUnit( |
||||
|
int plane, |
||||
|
int subsamplingX, |
||||
|
ReadOnlySpan<ushort> source, |
||||
|
Span<ushort> destination, |
||||
|
int planeWidth, |
||||
|
int planeHeight, |
||||
|
int horizontalStart, |
||||
|
int horizontalEnd, |
||||
|
int verticalStart, |
||||
|
int verticalEnd, |
||||
|
Av1LoopRestorationUnit unit, |
||||
|
Span<ushort> borderedSource, |
||||
|
Span<ushort> wienerScratch, |
||||
|
Span<int> 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<ushort> 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<ushort> 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; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Builds one filter source rectangle with normative horizontal and stripe-boundary extension.
|
||||
|
/// </summary>
|
||||
|
/// <param name="plane">The zero-based color-plane index.</param>
|
||||
|
/// <param name="frameStripe">The frame-relative processing-stripe index.</param>
|
||||
|
/// <param name="source">The immutable post-super-resolution plane samples.</param>
|
||||
|
/// <param name="planeWidth">The visible plane width.</param>
|
||||
|
/// <param name="planeHeight">The visible plane height.</param>
|
||||
|
/// <param name="blockStart">The first filtered plane column.</param>
|
||||
|
/// <param name="blockWidth">The number of filtered columns.</param>
|
||||
|
/// <param name="stripeStart">The first filtered plane row.</param>
|
||||
|
/// <param name="stripeHeight">The number of filtered rows.</param>
|
||||
|
/// <param name="destinationStride">The number of samples between bordered destination rows.</param>
|
||||
|
/// <param name="destination">The bordered filter source rectangle.</param>
|
||||
|
private void PopulateBorderedSource( |
||||
|
int plane, |
||||
|
int frameStripe, |
||||
|
ReadOnlySpan<ushort> source, |
||||
|
int planeWidth, |
||||
|
int planeHeight, |
||||
|
int blockStart, |
||||
|
int blockWidth, |
||||
|
int stripeStart, |
||||
|
int stripeHeight, |
||||
|
int destinationStride, |
||||
|
Span<ushort> destination) |
||||
|
{ |
||||
|
int stripeEnd = stripeStart + stripeHeight; |
||||
|
int sourceRowCount = stripeHeight + (FilterBorder * 2) + WienerPadding; |
||||
|
for (int destinationRow = 0; destinationRow < sourceRowCount; destinationRow++) |
||||
|
{ |
||||
|
int sourceY = stripeStart + destinationRow - FilterBorder; |
||||
|
ReadOnlySpan<ushort> 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<ushort> 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]); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Copies an unfiltered restoration-unit rectangle between plane working buffers.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The immutable source plane.</param>
|
||||
|
/// <param name="destination">The destination plane.</param>
|
||||
|
/// <param name="planeWidth">The number of samples between plane rows.</param>
|
||||
|
/// <param name="horizontalStart">The first copied column.</param>
|
||||
|
/// <param name="width">The number of copied columns.</param>
|
||||
|
/// <param name="verticalStart">The first copied row.</param>
|
||||
|
/// <param name="verticalEnd">The exclusive copied row limit.</param>
|
||||
|
private static void CopyRectangle( |
||||
|
ReadOnlySpan<ushort> source, |
||||
|
Span<ushort> 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..]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Copies one reconstructed plane into an immutable 16-bit working buffer.
|
||||
|
/// </summary>
|
||||
|
/// <param name="plane">The luma or chroma plane.</param>
|
||||
|
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
|
||||
|
/// <param name="subsamplingY">The vertical chroma subsampling shift.</param>
|
||||
|
/// <param name="planeWidth">The visible plane width.</param>
|
||||
|
/// <param name="planeHeight">The visible plane height.</param>
|
||||
|
/// <param name="destination">The row-major working buffer.</param>
|
||||
|
private void CopyPlaneToWorkingBuffer( |
||||
|
Av1Plane plane, |
||||
|
int subsamplingX, |
||||
|
int subsamplingY, |
||||
|
int planeWidth, |
||||
|
int planeHeight, |
||||
|
Span<ushort> destination) |
||||
|
{ |
||||
|
Span<byte> lowBitDepthPlane = default; |
||||
|
Span<ushort> highBitDepthPlane = default; |
||||
|
int sourceStride; |
||||
|
if (this.frameBuffer.BytesPerSample == 2) |
||||
|
{ |
||||
|
Span<short> signedPlane = this.frameBuffer.DeriveBlockPointer16( |
||||
|
plane, |
||||
|
Point.Empty, |
||||
|
subsamplingX, |
||||
|
subsamplingY, |
||||
|
out sourceStride); |
||||
|
|
||||
|
highBitDepthPlane = MemoryMarshal.Cast<short, ushort>(signedPlane); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
lowBitDepthPlane = this.frameBuffer.DeriveBlockPointer( |
||||
|
plane, |
||||
|
Point.Empty, |
||||
|
subsamplingX, |
||||
|
subsamplingY, |
||||
|
out sourceStride); |
||||
|
} |
||||
|
|
||||
|
for (int row = 0; row < planeHeight; row++) |
||||
|
{ |
||||
|
// DeriveBlockPointer spans begin on the row above the visible origin for prediction,
|
||||
|
// hence the leading stride in every frame-relative row offset.
|
||||
|
int frameOffset = sourceStride + (row * sourceStride); |
||||
|
Span<ushort> destinationRow = destination.Slice(row * planeWidth, planeWidth); |
||||
|
if (!highBitDepthPlane.IsEmpty) |
||||
|
{ |
||||
|
highBitDepthPlane.Slice(frameOffset, planeWidth).CopyTo(destinationRow); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
ReadOnlySpan<byte> sourceRow = lowBitDepthPlane.Slice(frameOffset, planeWidth); |
||||
|
for (int column = 0; column < planeWidth; column++) |
||||
|
{ |
||||
|
destinationRow[column] = sourceRow[column]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Copies one restored 16-bit working buffer back to its reconstructed plane.
|
||||
|
/// </summary>
|
||||
|
/// <param name="plane">The luma or chroma plane.</param>
|
||||
|
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
|
||||
|
/// <param name="subsamplingY">The vertical chroma subsampling shift.</param>
|
||||
|
/// <param name="planeWidth">The visible plane width.</param>
|
||||
|
/// <param name="planeHeight">The visible plane height.</param>
|
||||
|
/// <param name="source">The row-major restored working buffer.</param>
|
||||
|
private void CopyWorkingBufferToPlane( |
||||
|
Av1Plane plane, |
||||
|
int subsamplingX, |
||||
|
int subsamplingY, |
||||
|
int planeWidth, |
||||
|
int planeHeight, |
||||
|
ReadOnlySpan<ushort> source) |
||||
|
{ |
||||
|
Span<byte> lowBitDepthPlane = default; |
||||
|
Span<ushort> highBitDepthPlane = default; |
||||
|
int destinationStride; |
||||
|
if (this.frameBuffer.BytesPerSample == 2) |
||||
|
{ |
||||
|
Span<short> signedPlane = this.frameBuffer.DeriveBlockPointer16( |
||||
|
plane, |
||||
|
Point.Empty, |
||||
|
subsamplingX, |
||||
|
subsamplingY, |
||||
|
out destinationStride); |
||||
|
|
||||
|
highBitDepthPlane = MemoryMarshal.Cast<short, ushort>(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<ushort> sourceRow = source.Slice(row * planeWidth, planeWidth); |
||||
|
if (!highBitDepthPlane.IsEmpty) |
||||
|
{ |
||||
|
sourceRow.CopyTo(highBitDepthPlane[frameOffset..]); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
Span<byte> destinationRow = lowBitDepthPlane.Slice(frameOffset, planeWidth); |
||||
|
for (int column = 0; column < planeWidth; column++) |
||||
|
{ |
||||
|
destinationRow[column] = (byte)sourceRow[column]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue