Browse Source

Integrate AV1 super-resolution

pull/2633/head
James Jackson-South 2 months ago
parent
commit
84ba3f0883
  1. 3
      HEIF_IMPLEMENTATION_PLAN.md
  2. 2
      src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs
  3. 8
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs
  4. 7
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs
  5. 163
      src/ImageSharp/Formats/Heif/Av1/Pipeline/SuperResolution/Av1SuperResolutionDecoder.cs
  6. 182
      src/ImageSharp/Formats/Heif/Av1/Pipeline/SuperResolution/Av1SuperResolutionKernels.cs

3
HEIF_IMPLEMENTATION_PLAN.md

@ -73,6 +73,7 @@ This snapshot pins or classifies the available references and failures; it does
| `Av1TileReader` palette mode/color-map parsing, `Av1SymbolDecoder` palette distributions, `Av1BlockModeInfo` palette state, and `Av1PredictionDecoder` palette reconstruction | AV1 sections 5.11.46, 5.11.49, and 7.11.2 palette prediction | libaom `av1/decoder/decodemv.c`, `av1/decoder/detokenize.c`, `av1/decoder/decoder.h`, `av1/common/pred_common.c`, `av1/common/pred_common.h`, and `av1/common/entropymode.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Reuse the existing tile range decoder and frame-owned mode map while porting the normative palette cache merge, high-bit-depth color deltas, tile-adaptive mode/size/index distributions, diagonal color-map traversal, edge padding, and direct palette-sample reconstruction. This is AV1 still-image compression syntax and does not add retained video reference state or any ISO BMFF surface. |
| `Av1LoopFilterKernels`, `Av1LoopFilterContext`, and `Av1LoopFilterDecoder` | AV1 section 7.14 deblocking loop filter | libaom `aom_dsp/loopfilter.c` and `av1/common/av1_loopfilter.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Port the scalar 4-, 6-, 8-, and 14-tap low/high-bit-depth filters, sharpness thresholds, still-frame intra filter-level derivation, transform-edge selection, and plane traversal before enabling the stage. Later SIMD must preserve the scalar result. This is normative AV1 image reconstruction and adds neither generic ISO BMFF models nor retained video reference state. |
| `Av1CdefDecoder`, `Av1CdefKernels`, and CDEF-unit strength storage | AV1 sections 7.15.2 through 7.15.4 constrained directional enhancement filtering | libaom `av1/common/cdef.c`, `av1/common/cdef_block.c`, `av1/common/cdef.h`, and `av1/common/cdef_block.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Port the scalar direction search, variance adjustment, constrained primary/secondary taps, subsampling direction conversion, skipped-8x8 selection, and frame-edge sentinel behavior. Use a frame-owned source snapshot so filtering never consumes already modified samples. This is normative AV1 still-image reconstruction and introduces no ISO BMFF, track, timing, or sequence-playback surface. |
| `Av1SuperResolutionDecoder`, `Av1SuperResolutionKernels`, frame-size derivation, and decoded-image dimensions | AV1 sections 7.16 and 7.17 normative super-resolution upscaling | libaom `av1/common/resize.c`, `av1/common/resize.h`, `av1/common/convolve.c`, and `aom_dsp/aom_filter.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Port the fixed 64-phase, 8-tap horizontal filter, phase/step derivation, replicated frame edges, chroma width rounding, signed rounding, and 8/10/12-bit clipping. Reuse ImageSharp's existing cross-platform `Vector128_.MultiplyAddAdjacent` helper for the exact eight-coefficient dot product with a scalar fallback. Generic image resizing is not normative AV1 super-resolution. This adds no track, timing, fragment, animation, or generic ISO BMFF model. |
| `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. |
@ -120,7 +121,7 @@ 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. The available full-decode AV1 fixture does not exercise filtered transform edges or active CDEF strengths, so independent 8-, 10-, and 12-bit AVIF vectors remain required before either stage has external pixel-level verification. Super-resolution, 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 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.
- 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 and 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.
- 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.

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

@ -84,7 +84,7 @@ internal class Av1Decoder : IAv1TileReader
{
resultImage = new Image<TPixel>(
this.configuration,
this.FrameHeader.FrameSize.FrameWidth,
this.FrameHeader.FrameSize.SuperResolutionUpscaledWidth,
this.FrameHeader.FrameSize.FrameHeight,
null);

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

@ -636,13 +636,13 @@ internal class ObuReader
(frameHeader.FrameSize.SuperResolutionDenominator / 2)) /
frameHeader.FrameSize.SuperResolutionDenominator;
/*
if (frameHeader.FrameSize.SuperResolutionDenominator != Av1Constants.ScaleNumerator)
{
int manWidth = Math.Min(16, frameHeader.FrameSize.SuperResolutionUpscaledWidth);
frameHeader.FrameSize.FrameWidth = Math.Max(manWidth, frameHeader.FrameSize.FrameWidth);
// Appendix A requires an active super-resolution coded width of at least 16 samples,
// except when the signaled upscaled image itself is narrower than that minimum.
int minimumWidth = Math.Min(16, frameHeader.FrameSize.SuperResolutionUpscaledWidth);
frameHeader.FrameSize.FrameWidth = Math.Max(minimumWidth, frameHeader.FrameSize.FrameWidth);
}
*/
}
/// <summary>

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

@ -5,6 +5,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.Quantification;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.SuperResolution;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
@ -86,7 +87,7 @@ internal class Av1FrameDecoder : IAv1FrameDecoder
}
bool doLoopRestoration = false;
bool doUpscale = false;
bool doUpscale = this.frameHeader.FrameSize.FrameWidth != this.frameHeader.FrameSize.SuperResolutionUpscaledWidth;
Av1LoopFilterDecoder loopFilterDecoder = new(
this.sequenceHeader,
@ -105,7 +106,9 @@ internal class Av1FrameDecoder : IAv1FrameDecoder
Av1CdefDecoder cdefDecoder = new(this.sequenceHeader, this.frameHeader, this.frameInfo, this.frameBuffer);
cdefDecoder.DecodeFrame();
// SuperResolutionUpscaling(doUpscale);
Av1SuperResolutionDecoder superResolutionDecoder = new(this.sequenceHeader, this.frameHeader, this.frameBuffer);
superResolutionDecoder.DecodeFrame();
if (doLoopRestoration && doUpscale)
{
// LoopRestorationSaveBoundaryLines(true);

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

@ -0,0 +1,163 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.SuperResolution;
/// <summary>
/// Applies normative AV1 horizontal super-resolution upscaling to a reconstructed still-image frame.
/// </summary>
internal class Av1SuperResolutionDecoder
{
/// <summary>
/// The sequence-level bit-depth and color-plane configuration.
/// </summary>
private readonly ObuSequenceHeader sequenceHeader;
/// <summary>
/// The coded and upscaled frame dimensions.
/// </summary>
private readonly ObuFrameHeader frameHeader;
/// <summary>
/// The reconstructed planes updated with the upscaled samples.
/// </summary>
private readonly Av1FrameBuffer<byte> frameBuffer;
/// <summary>
/// Initializes a new instance of the <see cref="Av1SuperResolutionDecoder"/> class.
/// </summary>
/// <param name="sequenceHeader">The sequence header defining bit depth and color layout.</param>
/// <param name="frameHeader">The frame header defining coded and upscaled dimensions.</param>
/// <param name="frameBuffer">The CDEF-filtered frame samples to upscale.</param>
public Av1SuperResolutionDecoder(
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader,
Av1FrameBuffer<byte> frameBuffer)
{
this.sequenceHeader = sequenceHeader;
this.frameHeader = frameHeader;
this.frameBuffer = frameBuffer;
}
/// <summary>
/// Upscales each color plane horizontally and updates the visible frame dimensions.
/// </summary>
public void DecodeFrame()
{
ObuFrameSize frameSize = this.frameHeader.FrameSize;
int codedWidth = frameSize.FrameWidth;
int upscaledWidth = frameSize.SuperResolutionUpscaledWidth;
if (codedWidth != upscaledWidth)
{
ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig;
for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++)
{
Av1Plane plane = (Av1Plane)planeIndex;
int subsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0;
int subsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0;
this.UpscalePlane(plane, subsamplingX, subsamplingY, codedWidth, upscaledWidth, frameSize.FrameHeight);
}
}
// Plane-region consumers must observe the reconstructed frame rather than the sequence maxima
// used to size the reusable backing allocations.
this.frameBuffer.Width = upscaledWidth;
this.frameBuffer.Height = frameSize.FrameHeight;
}
/// <summary>
/// Upscales every row of one color plane with the normative fixed filter.
/// </summary>
/// <param name="plane">The color plane to upscale.</param>
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
/// <param name="subsamplingY">The vertical chroma subsampling shift.</param>
/// <param name="codedLumaWidth">The coded luma width before upscaling.</param>
/// <param name="upscaledLumaWidth">The luma width after upscaling.</param>
/// <param name="lumaHeight">The unchanged luma height.</param>
private void UpscalePlane(
Av1Plane plane,
int subsamplingX,
int subsamplingY,
int codedLumaWidth,
int upscaledLumaWidth,
int lumaHeight)
{
int codedWidth = Av1Math.DivideLog2Ceiling(codedLumaWidth, subsamplingX);
int upscaledWidth = Av1Math.DivideLog2Ceiling(upscaledLumaWidth, subsamplingX);
int reconstructedWidth = this.frameHeader.ModeInfoColumnCount << (Av1Constants.ModeInfoSizeLog2 - subsamplingX);
int height = Av1Math.DivideLog2Ceiling(lumaHeight, subsamplingY);
int step = Av1SuperResolutionKernels.GetConvolveStep(codedWidth, upscaledWidth);
int initialSubpixel = Av1SuperResolutionKernels.GetInitialSubpixel(codedWidth, upscaledWidth, step);
ushort[] sourceRow = new ushort[reconstructedWidth + (Av1SuperResolutionKernels.SourceBorder * 2)];
ushort[] outputRow = new ushort[upscaledWidth];
Span<byte> lowBitDepthPlane = default;
Span<ushort> highBitDepthPlane = default;
int stride;
if (this.frameBuffer.BytesPerSample == 2)
{
Span<short> signedPlane = this.frameBuffer.DeriveBlockPointer16(
plane,
Point.Empty,
subsamplingX,
subsamplingY,
out stride);
highBitDepthPlane = MemoryMarshal.Cast<short, ushort>(signedPlane);
}
else
{
lowBitDepthPlane = this.frameBuffer.DeriveBlockPointer(
plane,
Point.Empty,
subsamplingX,
subsamplingY,
out stride);
}
int sourceStart = Av1SuperResolutionKernels.SourceBorder;
int bitDepth = this.frameBuffer.BitDepth.GetBitCount();
// libaom partitions the same continuous phase progression by tile column but does not pad
// internal boundaries. Filtering the complete row therefore produces the identical samples.
for (int row = 0; row < height; row++)
{
Span<ushort> reconstructedSamples = sourceRow.AsSpan(sourceStart, reconstructedWidth);
int planeOffset = stride + (row * stride);
if (this.frameBuffer.BytesPerSample == 2)
{
highBitDepthPlane.Slice(planeOffset, reconstructedWidth).CopyTo(reconstructedSamples);
}
else
{
Span<byte> input = lowBitDepthPlane.Slice(planeOffset, reconstructedWidth);
for (int column = 0; column < reconstructedWidth; column++)
{
reconstructedSamples[column] = input[column];
}
}
// Phase derives from the exact coded width, but taps consume reconstruction through the
// enclosing 8-sample mode-info edge. Only samples beyond that aligned edge are replicated.
sourceRow.AsSpan(0, sourceStart).Fill(reconstructedSamples[0]);
sourceRow.AsSpan(sourceStart + reconstructedWidth, sourceStart).Fill(reconstructedSamples[^1]);
Av1SuperResolutionKernels.UpscaleRow(sourceRow, outputRow, step, initialSubpixel, bitDepth);
if (this.frameBuffer.BytesPerSample == 2)
{
outputRow.CopyTo(highBitDepthPlane.Slice(planeOffset, upscaledWidth));
}
else
{
Span<byte> output = lowBitDepthPlane.Slice(planeOffset, upscaledWidth);
for (int column = 0; column < upscaledWidth; column++)
{
output[column] = (byte)outputRow[column];
}
}
}
}
}

182
src/ImageSharp/Formats/Heif/Av1/Pipeline/SuperResolution/Av1SuperResolutionKernels.cs

@ -0,0 +1,182 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.SuperResolution;
/// <summary>
/// Provides the normative horizontal filter operations used by AV1 super-resolution upscaling.
/// </summary>
internal static class Av1SuperResolutionKernels
{
/// <summary>
/// The number of source samples consumed for each output sample.
/// </summary>
public const int FilterTapCount = 8;
/// <summary>
/// The number of replicated source samples reserved at each row edge.
/// </summary>
public const int SourceBorder = (FilterTapCount / 2) + 1;
/// <summary>
/// The number of fractional bits represented by each filter coefficient.
/// </summary>
private const int FilterBits = 7;
/// <summary>
/// The number of filter phases in the normative table, expressed as a base-two exponent.
/// </summary>
private const int SubpixelBits = 6;
/// <summary>
/// The number of fractional bits used by the horizontal sample position.
/// </summary>
private const int ScaleSubpixelBits = 14;
/// <summary>
/// The mask selecting the fractional sample position.
/// </summary>
private const int ScaleSubpixelMask = (1 << ScaleSubpixelBits) - 1;
/// <summary>
/// The number of low position bits omitted when selecting a filter phase.
/// </summary>
private const int ScaleExtraBits = ScaleSubpixelBits - SubpixelBits;
/// <summary>
/// The half-step bias applied before filter-phase selection.
/// </summary>
private const int ScaleExtraOffset = 1 << (ScaleExtraBits - 1);
/// <summary>
/// The flattened 64-phase, 8-tap normative AV1 super-resolution filter table.
/// </summary>
private static readonly short[] Filters =
[
0, 0, 0, 128, 0, 0, 0, 0, 0, 0, -1, 128, 2, -1, 0, 0,
0, 1, -3, 127, 4, -2, 1, 0, 0, 1, -4, 127, 6, -3, 1, 0,
0, 2, -6, 126, 8, -3, 1, 0, 0, 2, -7, 125, 11, -4, 1, 0,
-1, 2, -8, 125, 13, -5, 2, 0, -1, 3, -9, 124, 15, -6, 2, 0,
-1, 3, -10, 123, 18, -6, 2, -1, -1, 3, -11, 122, 20, -7, 3, -1,
-1, 4, -12, 121, 22, -8, 3, -1, -1, 4, -13, 120, 25, -9, 3, -1,
-1, 4, -14, 118, 28, -9, 3, -1, -1, 4, -15, 117, 30, -10, 4, -1,
-1, 5, -16, 116, 32, -11, 4, -1, -1, 5, -16, 114, 35, -12, 4, -1,
-1, 5, -17, 112, 38, -12, 4, -1, -1, 5, -18, 111, 40, -13, 5, -1,
-1, 5, -18, 109, 43, -14, 5, -1, -1, 6, -19, 107, 45, -14, 5, -1,
-1, 6, -19, 105, 48, -15, 5, -1, -1, 6, -19, 103, 51, -16, 5, -1,
-1, 6, -20, 101, 53, -16, 6, -1, -1, 6, -20, 99, 56, -17, 6, -1,
-1, 6, -20, 97, 58, -17, 6, -1, -1, 6, -20, 95, 61, -18, 6, -1,
-2, 7, -20, 93, 64, -18, 6, -2, -2, 7, -20, 91, 66, -19, 6, -1,
-2, 7, -20, 88, 69, -19, 6, -1, -2, 7, -20, 86, 71, -19, 6, -1,
-2, 7, -20, 84, 74, -20, 7, -2, -2, 7, -20, 81, 76, -20, 7, -1,
-2, 7, -20, 79, 79, -20, 7, -2, -1, 7, -20, 76, 81, -20, 7, -2,
-2, 7, -20, 74, 84, -20, 7, -2, -1, 6, -19, 71, 86, -20, 7, -2,
-1, 6, -19, 69, 88, -20, 7, -2, -1, 6, -19, 66, 91, -20, 7, -2,
-2, 6, -18, 64, 93, -20, 7, -2, -1, 6, -18, 61, 95, -20, 6, -1,
-1, 6, -17, 58, 97, -20, 6, -1, -1, 6, -17, 56, 99, -20, 6, -1,
-1, 6, -16, 53, 101, -20, 6, -1, -1, 5, -16, 51, 103, -19, 6, -1,
-1, 5, -15, 48, 105, -19, 6, -1, -1, 5, -14, 45, 107, -19, 6, -1,
-1, 5, -14, 43, 109, -18, 5, -1, -1, 5, -13, 40, 111, -18, 5, -1,
-1, 4, -12, 38, 112, -17, 5, -1, -1, 4, -12, 35, 114, -16, 5, -1,
-1, 4, -11, 32, 116, -16, 5, -1, -1, 4, -10, 30, 117, -15, 4, -1,
-1, 3, -9, 28, 118, -14, 4, -1, -1, 3, -9, 25, 120, -13, 4, -1,
-1, 3, -8, 22, 121, -12, 4, -1, -1, 3, -7, 20, 122, -11, 3, -1,
-1, 2, -6, 18, 123, -10, 3, -1, 0, 2, -6, 15, 124, -9, 3, -1,
0, 2, -5, 13, 125, -8, 2, -1, 0, 1, -4, 11, 125, -7, 2, 0,
0, 1, -3, 8, 126, -6, 2, 0, 0, 1, -3, 6, 127, -4, 1, 0,
0, 1, -2, 4, 127, -3, 1, 0, 0, 0, -1, 2, 128, -1, 0, 0
];
/// <summary>
/// Derives the fixed-point source-position increment for an output row.
/// </summary>
/// <param name="inputLength">The coded plane width.</param>
/// <param name="outputLength">The upscaled plane width.</param>
/// <returns>The source-position increment with 14 fractional bits.</returns>
public static int GetConvolveStep(int inputLength, int outputLength)
=> ((inputLength << ScaleSubpixelBits) + (outputLength / 2)) / outputLength;
/// <summary>
/// Derives the initial fixed-point source position for an output row.
/// </summary>
/// <param name="inputLength">The coded plane width.</param>
/// <param name="outputLength">The upscaled plane width.</param>
/// <param name="step">The source-position increment returned by <see cref="GetConvolveStep"/>.</param>
/// <returns>The initial source position with 14 fractional bits.</returns>
public static int GetInitialSubpixel(int inputLength, int outputLength, int step)
{
int error = (outputLength * step) - (inputLength << ScaleSubpixelBits);
int initial = (-((outputLength - inputLength) << (ScaleSubpixelBits - 1)) + (outputLength / 2)) /
outputLength;
initial += ScaleExtraOffset - (error / 2);
return initial & ScaleSubpixelMask;
}
/// <summary>
/// Upscales one replicated-edge source row with the normative AV1 filter.
/// </summary>
/// <param name="source">The coded row with <see cref="SourceBorder"/> replicated samples on each edge.</param>
/// <param name="destination">The upscaled destination row.</param>
/// <param name="step">The fixed-point source-position increment.</param>
/// <param name="initialSubpixel">The initial fixed-point source position.</param>
/// <param name="bitDepth">The encoded sample bit depth.</param>
public static void UpscaleRow(
ReadOnlySpan<ushort> source,
Span<ushort> destination,
int step,
int initialSubpixel,
int bitDepth)
{
int maximum = (1 << bitDepth) - 1;
int sourcePosition = initialSubpixel;
for (int column = 0; column < destination.Length; column++)
{
int integerPosition = sourcePosition >> ScaleSubpixelBits;
int filterPhase = (sourcePosition & ScaleSubpixelMask) >> ScaleExtraBits;
int sourceOffset = SourceBorder + integerPosition - (FilterTapCount / 2);
int filterOffset = filterPhase * FilterTapCount;
int sum = DotProduct(source, sourceOffset, filterOffset);
destination[column] = (ushort)Av1Math.Clip3(0, maximum, (sum + (1 << (FilterBits - 1))) >> FilterBits);
sourcePosition += step;
}
}
/// <summary>
/// Computes the exact signed eight-tap filter product for one output sample.
/// </summary>
/// <param name="source">The replicated-edge source row.</param>
/// <param name="sourceOffset">The first source sample consumed by the filter.</param>
/// <param name="filterOffset">The first coefficient in the selected filter phase.</param>
/// <returns>The unrounded filter sum.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int DotProduct(ReadOnlySpan<ushort> source, int sourceOffset, int filterOffset)
{
if (Vector128.IsHardwareAccelerated)
{
ref ushort sourceReference = ref MemoryMarshal.GetReference(source);
ref short filterReference = ref MemoryMarshal.GetArrayDataReference(Filters);
Vector128<short> samples = Vector128.LoadUnsafe(ref sourceReference, (nuint)sourceOffset).AsInt16();
Vector128<short> coefficients = Vector128.LoadUnsafe(ref filterReference, (nuint)filterOffset);
Vector128<int> pairSums = Vector128_.MultiplyAddAdjacent(samples, coefficients);
// Reuse ImageSharp's architecture-specific adjacent multiply/add and finish its four
// 32-bit lanes scalarly; this remains cheaper than constructing a per-pixel shuffle tree.
return pairSums.GetElement(0) + pairSums.GetElement(1) + pairSums.GetElement(2) + pairSums.GetElement(3);
}
int sum = 0;
for (int tap = 0; tap < FilterTapCount; tap++)
{
sum += source[sourceOffset + tap] * Filters[filterOffset + tap];
}
return sum;
}
}
Loading…
Cancel
Save