Browse Source

Implement SIMD-first AV1 deblocking

pull/2633/head
James Jackson-South 7 days ago
parent
commit
a0dfc25d4c
  1. 7
      HEIF_IMPLEMENTATION_PLAN.md
  2. 1079
      src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.cs
  3. 45
      src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterDecoder.cs
  4. 427
      src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterKernels.cs
  5. 145
      tests/ImageSharp.Benchmarks/Codecs/Heif/Av1DeblockingFilterBenchmarks.cs
  6. 476
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1DeblockingFilterTests.cs

7
HEIF_IMPLEMENTATION_PLAN.md

@ -267,7 +267,7 @@ This snapshot pins or classifies the available references and failures; it does
| `Av1FrameBuffer` high-bit-depth sample layout and `Av1YuvConverter` 10/12-bit packed-pixel conversion | AV1 section 6.4.1 bit depth and H.273 sample-range scaling | libaom `aom_scale/yv12config.h`, `av1/common/idct.c`, and `av1/common/reconintra.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f`; libavif `src/avif.c` and `src/reformat.c` at `092276ce89098ead06db80975173191e5fee1826` | Establish two-byte native sample storage with sample-unit strides for 10/12-bit reconstruction and use ImageSharp's existing `Rgb48` pixel-operation paths in both directions so packed-pixel staging does not reduce high-bit-depth samples to eight bits. |
| `Av1PredictionDecoder`, `Av1IntraPredictor`, `Av1ChromaFromLumaContext`, `Av1ChromaFromLumaPredictor`, `Av1PartitionInfo`, and the DC, directional, Paeth, smooth, filter-intra, and chroma-from-luma predictor operators | AV1 sections 7.11.2 and 7.11.2.3 intra prediction | libaom `aom_dsp/intrapred.c`, `av1/common/reconintra.c`, `av1/common/av1_common_int.h`, `av1/common/blockd.h`, `av1/common/cfl.c`, and `av1/common/cfl.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Design the shared predictor contract, sample layout, reference preparation, and scratch ownership for widest-to-narrowest SIMD traversal, then provide the equivalent scalar fallback through that contract. Preserve luma/chroma mode-neighbor addressing, directional upsampling, Paeth selection, smooth normalization, filter-intra taps, high-bit-depth clipping, chroma-from-luma storage/subsampling, and chroma-from-luma row strides. The WIP rectangular byte-pipeline smooth digest expectations encode width/height-swapped weights and must be replaced only from an independently generated oracle, not regenerated from this implementation. |
| `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` | Design the low/high-bit-depth edge layout and 4-, 6-, 8-, and 14-tap kernels for SIMD-first traversal, with sharpness thresholds, still-frame intra filter-level derivation, transform-edge selection, and plane traversal shared by the exact scalar fallback. Implement and verify both through the same contract before enabling the stage. This is normative AV1 image reconstruction and adds neither generic ISO BMFF models nor retained video reference state. |
| `Av1DeblockingFilter`, `Av1LoopFilterContext`, and `Av1LoopFilterDecoder` | AV1 section 7.14 deblocking loop filter | libaom `aom_dsp/loopfilter.c` and `av1/common/av1_loopfilter.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Design the 8- and high-bit-depth edge layout and 4-, 6-, 8-, and 14-tap kernels for SIMD-first traversal, with sharpness thresholds, still-frame intra filter-level derivation, transform-edge selection, and plane traversal shared by the exact scalar fallback. Implement and verify both through the same contract before enabling the stage. 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` | Design direction search and constrained primary/secondary filtering around vector-friendly block storage and SIMD-first kernels, with variance adjustment, subsampling direction conversion, skipped-8x8 selection, and frame-edge sentinel behavior shared by the scalar fallback. 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 section 7.16 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. |
| `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. |
@ -351,7 +351,7 @@ This assessment was reconciled with the source tree on 2026-08-26. Unless a resu
- 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.
- Transform coefficient entropy derivation and updates now address the above contexts relative to the tile column and the left contexts relative to the current superblock row, preserve luma coordinates independently of chroma subsampling, and test every packed context entry for the libaom any-nonzero rule. Extended vertical partition updates advance the mode-information column rather than the row. The existing multi-superblock 4:4:4 AVIF fixture now completes tile parsing; independent coefficient-context vectors across tile boundaries, chroma layouts, bit depths, and edge-clipped transforms remain required.
- 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 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. The visible still-image path then applies the complete self-contained film-grain parameter set after all in-loop filters. Independent 8-, 10-, and 12-bit AVIF vectors exercising every active filter and grain stage remain required before these paths have external pixel-level verification.
- 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 AV1 4-, 6-, 8-, and 14-tap deblocking kernels in normative vertical-then-horizontal order. Deblocking uses the same closed edge-operator architecture as the HEVC filter, with operators specialized by sample storage and orientation, `Vector128<int>` lanes representing the four rows or columns along an edge, and an allocation-free scalar fallback for disabled intrinsics. 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 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. The visible still-image path then applies the complete self-contained film-grain parameter set after all in-loop filters. Independent 8-, 10-, and 12-bit AVIF vectors exercising every active filter and grain 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, and clipped-edge padding. Reconstruction retains libaom's exact indexed-palette contract while using repeated eight-entry tables and native lane-local shuffles across runtime-selected `Vector512`, `Vector256`, and `Vector128` paths before an exact scalar fallback. `FeatureTestRunner` verifies palette sizes 2-8, transform widths 4-64, padded rows, and 8/10/12-bit samples. This path is not production-complete until its syntax and reconstruction are verified with an independently encoded palette AVIF fixture.
- Non-reduced still-image sequence parsing now consumes decoder-model operating parameters, temporal presentation fields, and OBU-layer-applicable buffer-removal fields only to preserve AV1 bit alignment. The scheduling values are not retained, and no movie, track, timing, playback, or generic ISO BMFF surface has been introduced. Existing focused sequence-header coverage exercises only reduced-still syntax, so an independent non-reduced still AVIF vector remains required.
- 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.
@ -502,6 +502,8 @@ Implement and verify in dependency order:
- [x] Route lossless 4x4 blocks through allocation-free reversible inverse Walsh-Hadamard reconstruction for 8/10/12-bit samples, including the DC-only specialization, `Vector128` production traversal, scalar fallback, exact clipping, and `FeatureTestRunner` parity.
- [ ] Verify lossless syntax, inverse quantization, prediction, and presented reconstruction with independently encoded 8/10/12-bit AVIF fixtures.
- [ ] Deblocking loop filter.
- [x] Implement allocation-free SIMD-first 4-, 6-, 8-, and 14-tap filtering for vertical and horizontal edges in 8/10/12-bit storage through closed edge operators, with exact scalar fallback and `FeatureTestRunner` parity against an independent definition.
- [ ] Verify deblocking syntax, filter-level derivation, boundary traversal, and presented reconstruction with independently encoded 8/10/12-bit AVIF fixtures.
- [ ] CDEF.
- [ ] Super-resolution scaling.
- [ ] Loop restoration.
@ -642,6 +644,7 @@ Tasks:
- [x] Add a permanent frame-wide AV1 chroma-from-luma prediction benchmark. The lane-selection run rejected the explicit `Vector512` experiment because AVX2 was 2.6% faster at 8-bit and 4.8% faster at 12-bit on the test system. With that path removed, the selected hardware and forced-AVX2 paths are equivalent; the final .NET 10 run measured 88.76 and 74.89 microseconds per padded 1920x1088 frame at 8 and 12 bits, compared with forced-scalar timings of 1.594 and 1.604 milliseconds: 18.0 and 21.4 times faster with zero managed allocations.
- [x] Add a permanent frame-wide AV1 palette-reconstruction benchmark. On .NET 10, runtime-selected hardware measured 20.40 and 62.99 microseconds per padded 1920x1088 frame at 8 and 12 bits, compared with forced-scalar timings of 488.96 and 509.58 microseconds: 24.0 and 8.1 times faster with zero managed allocations. Forced `Vector512` measured 20.81 and 62.70 microseconds, so production continues to honor the runtime's preferred vector width instead of overriding this machine's `Vector256` selection.
- [x] Add a permanent frame-wide AV1 lossless inverse-transform benchmark. Dense four-by-four reconstruction across a padded 1920x1088 frame measured 595.6 microseconds at 8 bits and 512.4 microseconds at 12 bits, compared with forced-scalar timings of 2.074 and 2.270 milliseconds: 3.5 and 4.4 times faster with zero managed allocations.
- [x] Add a permanent frame-wide AV1 deblocking benchmark. Complete vertical and horizontal fourteen-tap passes across a padded 1920x1088 frame measured 4.040 milliseconds at 8 bits and 4.184 milliseconds at 12 bits, compared with forced-scalar timings of 9.416 and 9.410 milliseconds: 2.33 and 2.25 times faster with zero managed allocations.
- [ ] Implement vector paths only for confirmed hot loops, using existing `Vector128`, `Vector256`, and `Vector512` helper and dispatch patterns where supported.
- [ ] Prioritize shared color conversion and pixel packing, chroma upsampling, inverse-transform add-and-clip, intra predictors, HEVC deblock/SAO, AV1 loop filter/CDEF/restoration, and contiguous grid copies.
- [ ] Benchmark the complete decode color pipeline on representative 8/10/12-bit AVIF and HEIC images with and without embedded ICC profiles. Report absolute end-to-end timings and allocations in addition to the isolated YUV/CICP and ICC stage costs.

1079
src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.cs

File diff suppressed because it is too large

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

@ -247,29 +247,50 @@ internal class Av1LoopFilterDecoder
int boundaryLimit = (2 * (filterLevel + 2)) + limit;
int highEdgeVarianceThreshold = filterLevel >> 4;
int q0Offset = stride + (planeY * stride) + planeX;
int pixelStep = verticalBoundary ? 1 : stride;
int lineStep = verticalBoundary ? stride : 1;
if (this.frameBuffer.BytesPerSample == 2)
{
Av1LoopFilterKernels.FilterHighBitDepthEdge(
highBitDepthSamples,
if (verticalBoundary)
{
Av1DeblockingFilter.FilterVertical(
highBitDepthSamples,
q0Offset,
stride,
kernelLength,
limit,
boundaryLimit,
highEdgeVarianceThreshold,
this.frameBuffer.BitDepth.GetBitCount());
}
else
{
Av1DeblockingFilter.FilterHorizontal(
highBitDepthSamples,
q0Offset,
stride,
kernelLength,
limit,
boundaryLimit,
highEdgeVarianceThreshold,
this.frameBuffer.BitDepth.GetBitCount());
}
}
else if (verticalBoundary)
{
Av1DeblockingFilter.FilterVertical(
lowBitDepthSamples,
q0Offset,
pixelStep,
lineStep,
stride,
kernelLength,
limit,
boundaryLimit,
highEdgeVarianceThreshold,
this.frameBuffer.BitDepth.GetBitCount());
highEdgeVarianceThreshold);
}
else
{
Av1LoopFilterKernels.FilterLowBitDepthEdge(
Av1DeblockingFilter.FilterHorizontal(
lowBitDepthSamples,
q0Offset,
pixelStep,
lineStep,
stride,
kernelLength,
limit,
boundaryLimit,

427
src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterKernels.cs

@ -1,427 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter;
/// <summary>
/// Applies the scalar AV1 deblocking kernels to low- and high-bit-depth sample edges.
/// </summary>
internal static class Av1LoopFilterKernels
{
/// <summary>
/// The number of samples processed along one transform edge at a time.
/// </summary>
private const int EdgeLaneCount = 4;
/// <summary>
/// The number of samples read by the widest AV1 deblocking kernel.
/// </summary>
private const int MaximumKernelSampleCount = 14;
/// <summary>
/// Applies one low-bit-depth deblocking kernel to four adjacent samples along an edge.
/// </summary>
/// <param name="samples">The plane storage containing the edge and its neighboring samples.</param>
/// <param name="q0Offset">The offset of the first sample on the Q side of the edge.</param>
/// <param name="pixelStep">The storage distance between adjacent samples across the edge.</param>
/// <param name="lineStep">The storage distance between adjacent samples along the edge.</param>
/// <param name="filterLength">The signaled 4-, 6-, 8-, or 14-tap filter length.</param>
/// <param name="limit">The threshold for adjacent samples on either side of the edge.</param>
/// <param name="boundaryLimit">The threshold for the discontinuity across the edge.</param>
/// <param name="highEdgeVarianceThreshold">The threshold that selects the narrow high-variance adjustment.</param>
public static void FilterLowBitDepthEdge(
Span<byte> samples,
int q0Offset,
int pixelStep,
int lineStep,
int filterLength,
int limit,
int boundaryLimit,
int highEdgeVarianceThreshold)
{
int radius = GetFilterRadius(filterLength);
Span<int> kernelSamples = stackalloc int[MaximumKernelSampleCount];
for (int lane = 0; lane < EdgeLaneCount; lane++)
{
int laneOffset = q0Offset + (lane * lineStep);
// Samples are centered in the scratch span as p6..p0,q0..q6. Loading only the selected radius
// avoids touching pixels that a narrow filter is not permitted to address near a frame boundary.
for (int distance = 1; distance <= radius; distance++)
{
kernelSamples[7 - distance] = samples[laneOffset - (distance * pixelStep)];
kernelSamples[6 + distance] = samples[laneOffset + ((distance - 1) * pixelStep)];
}
FilterSamples(
kernelSamples,
filterLength,
limit,
boundaryLimit,
highEdgeVarianceThreshold,
8);
for (int distance = 1; distance <= radius; distance++)
{
samples[laneOffset - (distance * pixelStep)] = (byte)kernelSamples[7 - distance];
samples[laneOffset + ((distance - 1) * pixelStep)] = (byte)kernelSamples[6 + distance];
}
}
}
/// <summary>
/// Applies one high-bit-depth deblocking kernel to four adjacent samples along an edge.
/// </summary>
/// <param name="samples">The plane storage containing the edge and its neighboring samples.</param>
/// <param name="q0Offset">The offset of the first sample on the Q side of the edge.</param>
/// <param name="pixelStep">The storage distance between adjacent samples across the edge.</param>
/// <param name="lineStep">The storage distance between adjacent samples along the edge.</param>
/// <param name="filterLength">The signaled 4-, 6-, 8-, or 14-tap filter length.</param>
/// <param name="limit">The eight-bit-domain threshold for adjacent samples on either side of the edge.</param>
/// <param name="boundaryLimit">The eight-bit-domain threshold for the discontinuity across the edge.</param>
/// <param name="highEdgeVarianceThreshold">The eight-bit-domain threshold that selects the narrow adjustment.</param>
/// <param name="bitDepth">The sample bit depth.</param>
public static void FilterHighBitDepthEdge(
Span<ushort> samples,
int q0Offset,
int pixelStep,
int lineStep,
int filterLength,
int limit,
int boundaryLimit,
int highEdgeVarianceThreshold,
int bitDepth)
{
int radius = GetFilterRadius(filterLength);
Span<int> kernelSamples = stackalloc int[MaximumKernelSampleCount];
for (int lane = 0; lane < EdgeLaneCount; lane++)
{
int laneOffset = q0Offset + (lane * lineStep);
// AV1 scales thresholds rather than samples for high-bit-depth filtering, so the exact reconstructed
// values are retained in the scratch span and shared with the low-bit-depth arithmetic below.
for (int distance = 1; distance <= radius; distance++)
{
kernelSamples[7 - distance] = samples[laneOffset - (distance * pixelStep)];
kernelSamples[6 + distance] = samples[laneOffset + ((distance - 1) * pixelStep)];
}
FilterSamples(
kernelSamples,
filterLength,
limit,
boundaryLimit,
highEdgeVarianceThreshold,
bitDepth);
for (int distance = 1; distance <= radius; distance++)
{
samples[laneOffset - (distance * pixelStep)] = (ushort)kernelSamples[7 - distance];
samples[laneOffset + ((distance - 1) * pixelStep)] = (ushort)kernelSamples[6 + distance];
}
}
}
/// <summary>
/// Gets the number of samples read from each side of an edge for a filter length.
/// </summary>
/// <param name="filterLength">The AV1 filter length.</param>
/// <returns>The sample radius on either side of the edge.</returns>
private static int GetFilterRadius(int filterLength) => filterLength switch
{
4 => 2,
6 => 3,
8 => 4,
14 => 7,
_ => 0
};
/// <summary>
/// Selects and applies the AV1 filter arithmetic for one edge lane.
/// </summary>
/// <param name="samples">The p6..p0,q0..q6 scratch samples.</param>
/// <param name="filterLength">The AV1 filter length.</param>
/// <param name="limit">The eight-bit-domain adjacent-sample threshold.</param>
/// <param name="boundaryLimit">The eight-bit-domain edge-discontinuity threshold.</param>
/// <param name="highEdgeVarianceThreshold">The eight-bit-domain high-edge-variance threshold.</param>
/// <param name="bitDepth">The sample bit depth.</param>
private static void FilterSamples(
Span<int> samples,
int filterLength,
int limit,
int boundaryLimit,
int highEdgeVarianceThreshold,
int bitDepth)
{
int thresholdScale = 1 << (bitDepth - 8);
int scaledLimit = limit * thresholdScale;
int scaledBoundaryLimit = boundaryLimit * thresholdScale;
bool filterEnabled;
bool flat;
switch (filterLength)
{
case 4:
filterEnabled = IsFilter2Enabled(samples, scaledLimit, scaledBoundaryLimit);
Filter4(samples, filterEnabled, highEdgeVarianceThreshold * thresholdScale, bitDepth);
break;
case 6:
filterEnabled = IsChromaFilterEnabled(samples, scaledLimit, scaledBoundaryLimit);
flat = IsChromaFlat(samples, thresholdScale);
Filter6(samples, filterEnabled, flat, highEdgeVarianceThreshold * thresholdScale, bitDepth);
break;
case 8:
filterEnabled = IsFilterEnabled(samples, scaledLimit, scaledBoundaryLimit);
flat = IsFlat(samples, thresholdScale);
Filter8(samples, filterEnabled, flat, highEdgeVarianceThreshold * thresholdScale, bitDepth);
break;
case 14:
filterEnabled = IsFilterEnabled(samples, scaledLimit, scaledBoundaryLimit);
flat = IsFlat(samples, thresholdScale);
bool outerFlat = IsOuterFlat(samples, thresholdScale);
Filter14(samples, filterEnabled, flat, outerFlat, highEdgeVarianceThreshold * thresholdScale, bitDepth);
break;
}
}
/// <summary>
/// Determines whether a 4-tap filter may modify an edge.
/// </summary>
/// <param name="samples">The p6..p0,q0..q6 scratch samples.</param>
/// <param name="limit">The scaled adjacent-sample threshold.</param>
/// <param name="boundaryLimit">The scaled edge-discontinuity threshold.</param>
/// <returns><see langword="true"/> when the edge satisfies the AV1 filter mask.</returns>
private static bool IsFilter2Enabled(ReadOnlySpan<int> samples, int limit, int boundaryLimit)
=> Math.Abs(samples[5] - samples[6]) <= limit &&
Math.Abs(samples[8] - samples[7]) <= limit &&
((2 * Math.Abs(samples[6] - samples[7])) + (Math.Abs(samples[5] - samples[8]) / 2)) <= boundaryLimit;
/// <summary>
/// Determines whether an 8- or 14-tap luma filter may modify an edge.
/// </summary>
/// <param name="samples">The p6..p0,q0..q6 scratch samples.</param>
/// <param name="limit">The scaled adjacent-sample threshold.</param>
/// <param name="boundaryLimit">The scaled edge-discontinuity threshold.</param>
/// <returns><see langword="true"/> when the edge satisfies the AV1 filter mask.</returns>
private static bool IsFilterEnabled(ReadOnlySpan<int> samples, int limit, int boundaryLimit)
=> Math.Abs(samples[3] - samples[4]) <= limit &&
Math.Abs(samples[4] - samples[5]) <= limit &&
Math.Abs(samples[5] - samples[6]) <= limit &&
Math.Abs(samples[8] - samples[7]) <= limit &&
Math.Abs(samples[9] - samples[8]) <= limit &&
Math.Abs(samples[10] - samples[9]) <= limit &&
((2 * Math.Abs(samples[6] - samples[7])) + (Math.Abs(samples[5] - samples[8]) / 2)) <= boundaryLimit;
/// <summary>
/// Determines whether a 6-tap chroma filter may modify an edge.
/// </summary>
/// <param name="samples">The p6..p0,q0..q6 scratch samples.</param>
/// <param name="limit">The scaled adjacent-sample threshold.</param>
/// <param name="boundaryLimit">The scaled edge-discontinuity threshold.</param>
/// <returns><see langword="true"/> when the edge satisfies the AV1 chroma filter mask.</returns>
private static bool IsChromaFilterEnabled(ReadOnlySpan<int> samples, int limit, int boundaryLimit)
=> Math.Abs(samples[4] - samples[5]) <= limit &&
Math.Abs(samples[5] - samples[6]) <= limit &&
Math.Abs(samples[8] - samples[7]) <= limit &&
Math.Abs(samples[9] - samples[8]) <= limit &&
((2 * Math.Abs(samples[6] - samples[7])) + (Math.Abs(samples[5] - samples[8]) / 2)) <= boundaryLimit;
/// <summary>
/// Determines whether the inner four samples on each side form a flat edge.
/// </summary>
/// <param name="samples">The p6..p0,q0..q6 scratch samples.</param>
/// <param name="threshold">The scaled flatness threshold.</param>
/// <returns><see langword="true"/> when the samples satisfy the AV1 flatness mask.</returns>
private static bool IsFlat(ReadOnlySpan<int> samples, int threshold)
=> Math.Abs(samples[5] - samples[6]) <= threshold &&
Math.Abs(samples[8] - samples[7]) <= threshold &&
Math.Abs(samples[4] - samples[6]) <= threshold &&
Math.Abs(samples[9] - samples[7]) <= threshold &&
Math.Abs(samples[3] - samples[6]) <= threshold &&
Math.Abs(samples[10] - samples[7]) <= threshold;
/// <summary>
/// Determines whether the three chroma samples on each side form a flat edge.
/// </summary>
/// <param name="samples">The p6..p0,q0..q6 scratch samples.</param>
/// <param name="threshold">The scaled flatness threshold.</param>
/// <returns><see langword="true"/> when the samples satisfy the AV1 chroma flatness mask.</returns>
private static bool IsChromaFlat(ReadOnlySpan<int> samples, int threshold)
=> Math.Abs(samples[5] - samples[6]) <= threshold &&
Math.Abs(samples[8] - samples[7]) <= threshold &&
Math.Abs(samples[4] - samples[6]) <= threshold &&
Math.Abs(samples[9] - samples[7]) <= threshold;
/// <summary>
/// Determines whether the outer samples permit the 14-tap flat filter.
/// </summary>
/// <param name="samples">The p6..p0,q0..q6 scratch samples.</param>
/// <param name="threshold">The scaled flatness threshold.</param>
/// <returns><see langword="true"/> when the outer samples satisfy the AV1 flatness mask.</returns>
private static bool IsOuterFlat(ReadOnlySpan<int> samples, int threshold)
=> Math.Abs(samples[1] - samples[6]) <= threshold &&
Math.Abs(samples[11] - samples[7]) <= threshold &&
Math.Abs(samples[2] - samples[6]) <= threshold &&
Math.Abs(samples[12] - samples[7]) <= threshold &&
Math.Abs(samples[0] - samples[6]) <= threshold &&
Math.Abs(samples[13] - samples[7]) <= threshold;
/// <summary>
/// Applies the narrow signed-saturating AV1 edge adjustment.
/// </summary>
/// <param name="samples">The p6..p0,q0..q6 scratch samples.</param>
/// <param name="filterEnabled">Whether the filter mask permits modification.</param>
/// <param name="highEdgeVarianceThreshold">The scaled high-edge-variance threshold.</param>
/// <param name="bitDepth">The sample bit depth.</param>
private static void Filter4(Span<int> samples, bool filterEnabled, int highEdgeVarianceThreshold, int bitDepth)
{
if (!filterEnabled)
{
return;
}
int signedOffset = 128 << (bitDepth - 8);
int signedMinimum = -signedOffset;
int signedMaximum = signedOffset - 1;
int p1 = samples[5] - signedOffset;
int p0 = samples[6] - signedOffset;
int q0 = samples[7] - signedOffset;
int q1 = samples[8] - signedOffset;
bool highEdgeVariance = Math.Abs(samples[5] - samples[6]) > highEdgeVarianceThreshold ||
Math.Abs(samples[8] - samples[7]) > highEdgeVarianceThreshold;
// The saturating signed-domain arithmetic is normative. Clipping only the final samples is not equivalent
// because the intermediate delta can saturate before the asymmetric +4/+3 rounding is applied.
int filter = highEdgeVariance ? Math.Clamp(p1 - q1, signedMinimum, signedMaximum) : 0;
filter = Math.Clamp(filter + (3 * (q0 - p0)), signedMinimum, signedMaximum);
int filter1 = Math.Clamp(filter + 4, signedMinimum, signedMaximum) >> 3;
int filter2 = Math.Clamp(filter + 3, signedMinimum, signedMaximum) >> 3;
samples[7] = Math.Clamp(q0 - filter1, signedMinimum, signedMaximum) + signedOffset;
samples[6] = Math.Clamp(p0 + filter2, signedMinimum, signedMaximum) + signedOffset;
int outerFilter = highEdgeVariance ? 0 : (filter1 + 1) >> 1;
samples[8] = Math.Clamp(q1 - outerFilter, signedMinimum, signedMaximum) + signedOffset;
samples[5] = Math.Clamp(p1 + outerFilter, signedMinimum, signedMaximum) + signedOffset;
}
/// <summary>
/// Applies the AV1 6-tap chroma filter or its 4-tap fallback.
/// </summary>
/// <param name="samples">The p6..p0,q0..q6 scratch samples.</param>
/// <param name="filterEnabled">Whether the filter mask permits modification.</param>
/// <param name="flat">Whether the samples permit the wider flat filter.</param>
/// <param name="highEdgeVarianceThreshold">The scaled high-edge-variance threshold.</param>
/// <param name="bitDepth">The sample bit depth.</param>
private static void Filter6(Span<int> samples, bool filterEnabled, bool flat, int highEdgeVarianceThreshold, int bitDepth)
{
if (filterEnabled && flat)
{
int p2 = samples[4];
int p1 = samples[5];
int p0 = samples[6];
int q0 = samples[7];
int q1 = samples[8];
int q2 = samples[9];
samples[5] = RoundPowerOfTwo((3 * p2) + (2 * p1) + (2 * p0) + q0, 3);
samples[6] = RoundPowerOfTwo(p2 + (2 * p1) + (2 * p0) + (2 * q0) + q1, 3);
samples[7] = RoundPowerOfTwo(p1 + (2 * p0) + (2 * q0) + (2 * q1) + q2, 3);
samples[8] = RoundPowerOfTwo(p0 + (2 * q0) + (2 * q1) + (3 * q2), 3);
return;
}
Filter4(samples, filterEnabled, highEdgeVarianceThreshold, bitDepth);
}
/// <summary>
/// Applies the AV1 8-tap luma filter or its 4-tap fallback.
/// </summary>
/// <param name="samples">The p6..p0,q0..q6 scratch samples.</param>
/// <param name="filterEnabled">Whether the filter mask permits modification.</param>
/// <param name="flat">Whether the samples permit the wider flat filter.</param>
/// <param name="highEdgeVarianceThreshold">The scaled high-edge-variance threshold.</param>
/// <param name="bitDepth">The sample bit depth.</param>
private static void Filter8(Span<int> samples, bool filterEnabled, bool flat, int highEdgeVarianceThreshold, int bitDepth)
{
if (filterEnabled && flat)
{
int p3 = samples[3];
int p2 = samples[4];
int p1 = samples[5];
int p0 = samples[6];
int q0 = samples[7];
int q1 = samples[8];
int q2 = samples[9];
int q3 = samples[10];
samples[4] = RoundPowerOfTwo((3 * p3) + (2 * p2) + p1 + p0 + q0, 3);
samples[5] = RoundPowerOfTwo((2 * p3) + p2 + (2 * p1) + p0 + q0 + q1, 3);
samples[6] = RoundPowerOfTwo(p3 + p2 + p1 + (2 * p0) + q0 + q1 + q2, 3);
samples[7] = RoundPowerOfTwo(p2 + p1 + p0 + (2 * q0) + q1 + q2 + q3, 3);
samples[8] = RoundPowerOfTwo(p1 + p0 + q0 + (2 * q1) + q2 + (2 * q3), 3);
samples[9] = RoundPowerOfTwo(p0 + q0 + q1 + (2 * q2) + (3 * q3), 3);
return;
}
Filter4(samples, filterEnabled, highEdgeVarianceThreshold, bitDepth);
}
/// <summary>
/// Applies the AV1 14-tap luma filter or its 8-/4-tap fallback.
/// </summary>
/// <param name="samples">The p6..p0,q0..q6 scratch samples.</param>
/// <param name="filterEnabled">Whether the filter mask permits modification.</param>
/// <param name="flat">Whether the inner samples permit a wider flat filter.</param>
/// <param name="outerFlat">Whether the outer samples permit the 14-tap flat filter.</param>
/// <param name="highEdgeVarianceThreshold">The scaled high-edge-variance threshold.</param>
/// <param name="bitDepth">The sample bit depth.</param>
private static void Filter14(Span<int> samples, bool filterEnabled, bool flat, bool outerFlat, int highEdgeVarianceThreshold, int bitDepth)
{
if (filterEnabled && flat && outerFlat)
{
int p6 = samples[0];
int p5 = samples[1];
int p4 = samples[2];
int p3 = samples[3];
int p2 = samples[4];
int p1 = samples[5];
int p0 = samples[6];
int q0 = samples[7];
int q1 = samples[8];
int q2 = samples[9];
int q3 = samples[10];
int q4 = samples[11];
int q5 = samples[12];
int q6 = samples[13];
// The repeated endpoint weights extend the normative 13-tap window without reading beyond p6/q6.
samples[1] = RoundPowerOfTwo((7 * p6) + (2 * p5) + (2 * p4) + p3 + p2 + p1 + p0 + q0, 4);
samples[2] = RoundPowerOfTwo((5 * p6) + (2 * p5) + (2 * p4) + (2 * p3) + p2 + p1 + p0 + q0 + q1, 4);
samples[3] = RoundPowerOfTwo((4 * p6) + p5 + (2 * p4) + (2 * p3) + (2 * p2) + p1 + p0 + q0 + q1 + q2, 4);
samples[4] = RoundPowerOfTwo((3 * p6) + p5 + p4 + (2 * p3) + (2 * p2) + (2 * p1) + p0 + q0 + q1 + q2 + q3, 4);
samples[5] = RoundPowerOfTwo((2 * p6) + p5 + p4 + p3 + (2 * p2) + (2 * p1) + (2 * p0) + q0 + q1 + q2 + q3 + q4, 4);
samples[6] = RoundPowerOfTwo(p6 + p5 + p4 + p3 + p2 + (2 * p1) + (2 * p0) + (2 * q0) + q1 + q2 + q3 + q4 + q5, 4);
samples[7] = RoundPowerOfTwo(p5 + p4 + p3 + p2 + p1 + (2 * p0) + (2 * q0) + (2 * q1) + q2 + q3 + q4 + q5 + q6, 4);
samples[8] = RoundPowerOfTwo(p4 + p3 + p2 + p1 + p0 + (2 * q0) + (2 * q1) + (2 * q2) + q3 + q4 + q5 + (2 * q6), 4);
samples[9] = RoundPowerOfTwo(p3 + p2 + p1 + p0 + q0 + (2 * q1) + (2 * q2) + (2 * q3) + q4 + q5 + (3 * q6), 4);
samples[10] = RoundPowerOfTwo(p2 + p1 + p0 + q0 + q1 + (2 * q2) + (2 * q3) + (2 * q4) + q5 + (4 * q6), 4);
samples[11] = RoundPowerOfTwo(p1 + p0 + q0 + q1 + q2 + (2 * q3) + (2 * q4) + (2 * q5) + (5 * q6), 4);
samples[12] = RoundPowerOfTwo(p0 + q0 + q1 + q2 + q3 + (2 * q4) + (2 * q5) + (7 * q6), 4);
return;
}
Filter8(samples, filterEnabled, flat, highEdgeVarianceThreshold, bitDepth);
}
/// <summary>
/// Rounds an integer while dividing by a power of two.
/// </summary>
/// <param name="value">The integer to round.</param>
/// <param name="bitCount">The base-two divisor exponent.</param>
/// <returns>The rounded quotient.</returns>
private static int RoundPowerOfTwo(int value, int bitCount)
=> (value + (1 << (bitCount - 1))) >> bitCount;
}

145
tests/ImageSharp.Benchmarks/Codecs/Heif/Av1DeblockingFilterBenchmarks.cs

@ -0,0 +1,145 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter;
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif;
/// <summary>
/// Measures AV1 deblocking across a padded full-HD reconstruction surface.
/// </summary>
[Config(typeof(Configuration))]
[MemoryDiagnoser(displayGenColumns: false)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
public class Av1DeblockingFilterBenchmarks
{
/// <summary>
/// The visible coded frame width in samples.
/// </summary>
private const int Width = 1920;
/// <summary>
/// The coded frame height including the final padded coding-tree row for a 1080-line presentation.
/// </summary>
private const int Height = 1088;
/// <summary>
/// The border reserved around the visible reconstruction surface.
/// </summary>
private const int Padding = 16;
/// <summary>
/// The number of samples between adjacent padded rows.
/// </summary>
private const int Stride = Width + (2 * Padding);
/// <summary>
/// The eight-bit padded reconstruction surface.
/// </summary>
private readonly byte[] samples8 = new byte[Stride * (Height + (2 * Padding))];
/// <summary>
/// The twelve-bit padded reconstruction surface.
/// </summary>
private readonly ushort[] samples12 = new ushort[Stride * (Height + (2 * Padding))];
/// <summary>
/// Populates smooth deterministic samples that exercise enabled narrow and wide filter masks.
/// </summary>
[GlobalSetup]
public void Setup()
{
for (int row = 0; row < Height + (2 * Padding); row++)
{
for (int column = 0; column < Stride; column++)
{
int value = 96 + ((row + (2 * column)) & 15);
int offset = (row * Stride) + column;
this.samples8[offset] = (byte)value;
this.samples12[offset] = (ushort)(value << 4);
}
}
}
/// <summary>
/// Measures both AV1 deblocking passes over an eight-bit full-HD frame.
/// </summary>
/// <returns>A filtered sample that keeps the output observable.</returns>
[Benchmark]
[BenchmarkCategory("8Bit")]
public byte Filter8BitFrame()
{
for (int y = 0; y < Height; y += 4)
{
for (int x = 8; x < Width; x += 8)
{
int q0Offset = ((Padding + y) * Stride) + Padding + x;
Av1DeblockingFilter.FilterVertical(this.samples8, q0Offset, Stride, 14, 20, 60, 3);
}
}
for (int y = 8; y < Height; y += 8)
{
for (int x = 0; x < Width; x += 4)
{
int q0Offset = ((Padding + y) * Stride) + Padding + x;
Av1DeblockingFilter.FilterHorizontal(this.samples8, q0Offset, Stride, 14, 20, 60, 3);
}
}
return this.samples8[(Padding * Stride) + Padding];
}
/// <summary>
/// Measures both AV1 deblocking passes over a twelve-bit full-HD frame.
/// </summary>
/// <returns>A filtered sample that keeps the output observable.</returns>
[Benchmark]
[BenchmarkCategory("12Bit")]
public ushort Filter12BitFrame()
{
for (int y = 0; y < Height; y += 4)
{
for (int x = 8; x < Width; x += 8)
{
int q0Offset = ((Padding + y) * Stride) + Padding + x;
Av1DeblockingFilter.FilterVertical(this.samples12, q0Offset, Stride, 14, 20, 60, 3, 12);
}
}
for (int y = 8; y < Height; y += 8)
{
for (int x = 0; x < Width; x += 4)
{
int q0Offset = ((Padding + y) * Stride) + Padding + x;
Av1DeblockingFilter.FilterHorizontal(this.samples12, q0Offset, Stride, 14, 20, 60, 3, 12);
}
}
return this.samples12[(Padding * Stride) + Padding];
}
/// <summary>
/// Configures production-process measurements for hardware and scalar filtering.
/// </summary>
public sealed class Configuration : ManualConfig
{
/// <summary>
/// Initializes a new instance of the <see cref="Configuration"/> class.
/// </summary>
public Configuration()
{
this.AddJob(Job.ShortRun.WithId("Hardware").AsBaseline());
this.AddJob(
Job.ShortRun
.WithId("Scalar")
.WithEnvironmentVariable("DOTNET_EnableHWIntrinsic", "0"));
}
}
}

476
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1DeblockingFilterTests.cs

@ -0,0 +1,476 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
/// <summary>
/// Verifies AV1 deblocking across every filter width, sample precision, orientation, and intrinsic tier.
/// </summary>
[Trait("Format", "Avif")]
public class Av1DeblockingFilterTests
{
/// <summary>
/// The hardware configurations required to exercise packed filtering and the scalar fallback.
/// </summary>
private const HwIntrinsics Configurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic;
/// <summary>
/// The padded plane width used to expose horizontal and vertical edge traversal.
/// </summary>
private const int Stride = 32;
/// <summary>
/// The first Q-side coordinate, leaving the widest kernel addressable on every side.
/// </summary>
private const int EdgeCoordinate = 12;
/// <summary>
/// Verifies exact filtering and untouched padding against an independent scalar definition.
/// </summary>
[Fact]
public void FilterMatchesIndependentDefinitionAcrossIntrinsicTiers()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateFilters, Configurations);
/// <summary>
/// Exercises mixed flatness, high-edge-variance, disabled-mask, direction, and bit-depth cases.
/// </summary>
private static void ValidateFilters()
{
int[] filterLengths = [4, 6, 8, 14];
foreach (int bitDepth in new[] { 8, 10, 12 })
{
int scale = 1 << (bitDepth - 8);
int[][] mixedWindows = CreateMixedWindows(scale);
int[][] disabledWindows = CreateDisabledWindows(bitDepth);
foreach (bool vertical in new[] { true, false })
{
foreach (int filterLength in filterLengths)
{
if (bitDepth == 8)
{
AssertByteFilter(vertical, filterLength, mixedWindows);
AssertByteFilter(vertical, filterLength, disabledWindows);
}
else
{
AssertUInt16Filter(vertical, filterLength, bitDepth, mixedWindows);
AssertUInt16Filter(vertical, filterLength, bitDepth, disabledWindows);
}
}
}
}
}
/// <summary>
/// Verifies one eight-bit filter configuration against the independent definition.
/// </summary>
private static void AssertByteFilter(bool vertical, int filterLength, int[][] windows)
{
byte[] expected = Enumerable.Repeat((byte)231, Stride * Stride).ToArray();
Populate(expected, vertical, windows);
byte[] actual = (byte[])expected.Clone();
int q0Offset = (EdgeCoordinate * Stride) + EdgeCoordinate;
ApplyReference(expected, vertical, q0Offset, filterLength, 20, 60, 3, 8);
if (vertical)
{
Av1DeblockingFilter.FilterVertical(actual, q0Offset, Stride, filterLength, 20, 60, 3);
}
else
{
Av1DeblockingFilter.FilterHorizontal(actual, q0Offset, Stride, filterLength, 20, 60, 3);
}
Assert.Equal(expected, actual);
}
/// <summary>
/// Verifies one high-bit-depth filter configuration against the independent definition.
/// </summary>
private static void AssertUInt16Filter(bool vertical, int filterLength, int bitDepth, int[][] windows)
{
ushort[] expected = Enumerable.Repeat((ushort)60000, Stride * Stride).ToArray();
Populate(expected, vertical, windows);
ushort[] actual = (ushort[])expected.Clone();
int q0Offset = (EdgeCoordinate * Stride) + EdgeCoordinate;
ApplyReference(expected, vertical, q0Offset, filterLength, 20, 60, 3, bitDepth);
if (vertical)
{
Av1DeblockingFilter.FilterVertical(actual, q0Offset, Stride, filterLength, 20, 60, 3, bitDepth);
}
else
{
Av1DeblockingFilter.FilterHorizontal(actual, q0Offset, Stride, filterLength, 20, 60, 3, bitDepth);
}
Assert.Equal(expected, actual);
}
/// <summary>
/// Creates four lanes that independently select the wide, shorter-wide, narrow, and high-variance results.
/// </summary>
private static int[][] CreateMixedWindows(int scale)
=>
[
Scale([100, 100, 100, 100, 100, 100, 100, 101, 101, 101, 101, 101, 101, 101], scale),
Scale([94, 94, 94, 100, 100, 100, 100, 101, 101, 101, 101, 107, 107, 107], scale),
Scale([86, 88, 90, 92, 94, 96, 98, 102, 104, 106, 108, 110, 112, 114], scale),
Scale([85, 85, 85, 85, 85, 85, 100, 104, 119, 119, 119, 119, 119, 119], scale)
];
/// <summary>
/// Creates four lanes whose cross-edge discontinuity disables every filter width.
/// </summary>
private static int[][] CreateDisabledWindows(int bitDepth)
{
int maximum = (1 << bitDepth) - 1;
int[] window = [0, 0, 0, 0, 0, 0, 0, maximum, maximum, maximum, maximum, maximum, maximum, maximum];
return [(int[])window.Clone(), (int[])window.Clone(), (int[])window.Clone(), (int[])window.Clone()];
}
/// <summary>
/// Scales an eight-bit-domain sample window to the requested coded precision.
/// </summary>
private static int[] Scale(int[] values, int scale)
{
for (int index = 0; index < values.Length; index++)
{
values[index] *= scale;
}
return values;
}
/// <summary>
/// Places four eight-bit p6..q6 windows along one padded edge.
/// </summary>
private static void Populate(Span<byte> samples, bool vertical, int[][] windows)
{
for (int index = 0; index < 4; index++)
{
for (int sample = 0; sample < 14; sample++)
{
int distance = sample - 7;
int offset = vertical
? ((EdgeCoordinate + index) * Stride) + EdgeCoordinate + distance
: ((EdgeCoordinate + distance) * Stride) + EdgeCoordinate + index;
samples[offset] = (byte)windows[index][sample];
}
}
}
/// <summary>
/// Places four 16-bit p6..q6 windows along one padded edge.
/// </summary>
private static void Populate(Span<ushort> samples, bool vertical, int[][] windows)
{
for (int index = 0; index < 4; index++)
{
for (int sample = 0; sample < 14; sample++)
{
int distance = sample - 7;
int offset = vertical
? ((EdgeCoordinate + index) * Stride) + EdgeCoordinate + distance
: ((EdgeCoordinate + distance) * Stride) + EdgeCoordinate + index;
samples[offset] = (ushort)windows[index][sample];
}
}
}
/// <summary>
/// Applies the scalar AV1 definition to four eight-bit samples along one edge.
/// </summary>
private static void ApplyReference(
Span<byte> samples,
bool vertical,
int q0Offset,
int filterLength,
int limit,
int boundaryLimit,
int highEdgeVarianceThreshold,
int bitDepth)
{
for (int index = 0; index < 4; index++)
{
int[] window = LoadWindow(samples, vertical, q0Offset, index);
FilterReference(window, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, bitDepth);
StoreWindow(samples, vertical, q0Offset, index, filterLength, window);
}
}
/// <summary>
/// Applies the scalar AV1 definition to four 16-bit samples along one edge.
/// </summary>
private static void ApplyReference(
Span<ushort> samples,
bool vertical,
int q0Offset,
int filterLength,
int limit,
int boundaryLimit,
int highEdgeVarianceThreshold,
int bitDepth)
{
for (int index = 0; index < 4; index++)
{
int[] window = LoadWindow(samples, vertical, q0Offset, index);
FilterReference(window, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, bitDepth);
StoreWindow(samples, vertical, q0Offset, index, filterLength, window);
}
}
/// <summary>
/// Loads one eight-bit p6..q6 window independently of the production edge operators.
/// </summary>
private static int[] LoadWindow(ReadOnlySpan<byte> samples, bool vertical, int q0Offset, int index)
{
int[] result = new int[14];
for (int sample = 0; sample < result.Length; sample++)
{
int distance = sample - 7;
int offset = vertical ? q0Offset + (index * Stride) + distance : q0Offset + (distance * Stride) + index;
result[sample] = samples[offset];
}
return result;
}
/// <summary>
/// Loads one 16-bit p6..q6 window independently of the production edge operators.
/// </summary>
private static int[] LoadWindow(ReadOnlySpan<ushort> samples, bool vertical, int q0Offset, int index)
{
int[] result = new int[14];
for (int sample = 0; sample < result.Length; sample++)
{
int distance = sample - 7;
int offset = vertical ? q0Offset + (index * Stride) + distance : q0Offset + (distance * Stride) + index;
result[sample] = samples[offset];
}
return result;
}
/// <summary>
/// Stores every potentially modified sample from one eight-bit reference window.
/// </summary>
private static void StoreWindow(Span<byte> samples, bool vertical, int q0Offset, int index, int filterLength, ReadOnlySpan<int> window)
{
int radius = filterLength switch
{
4 or 6 => 2,
8 => 3,
_ => 6
};
for (int distance = -radius; distance < radius; distance++)
{
int offset = vertical ? q0Offset + (index * Stride) + distance : q0Offset + (distance * Stride) + index;
samples[offset] = (byte)window[distance + 7];
}
}
/// <summary>
/// Stores every potentially modified sample from one 16-bit reference window.
/// </summary>
private static void StoreWindow(Span<ushort> samples, bool vertical, int q0Offset, int index, int filterLength, ReadOnlySpan<int> window)
{
int radius = filterLength switch
{
4 or 6 => 2,
8 => 3,
_ => 6
};
for (int distance = -radius; distance < radius; distance++)
{
int offset = vertical ? q0Offset + (index * Stride) + distance : q0Offset + (distance * Stride) + index;
samples[offset] = (ushort)window[distance + 7];
}
}
/// <summary>
/// Selects and applies the normative scalar kernel for one p6..q6 window.
/// </summary>
private static void FilterReference(
Span<int> samples,
int filterLength,
int limit,
int boundaryLimit,
int highEdgeVarianceThreshold,
int bitDepth)
{
int scale = 1 << (bitDepth - 8);
int scaledLimit = limit * scale;
int scaledBoundaryLimit = boundaryLimit * scale;
bool filterEnabled = IsFilterEnabled(samples, filterLength, scaledLimit, scaledBoundaryLimit);
if (filterLength == 6 && filterEnabled && IsFlat(samples, filterLength, scale))
{
int p2 = samples[4];
int p1 = samples[5];
int p0 = samples[6];
int q0 = samples[7];
int q1 = samples[8];
int q2 = samples[9];
samples[5] = ((3 * p2) + (2 * p1) + (2 * p0) + q0 + 4) >> 3;
samples[6] = (p2 + (2 * p1) + (2 * p0) + (2 * q0) + q1 + 4) >> 3;
samples[7] = (p1 + (2 * p0) + (2 * q0) + (2 * q1) + q2 + 4) >> 3;
samples[8] = (p0 + (2 * q0) + (2 * q1) + (3 * q2) + 4) >> 3;
return;
}
bool flat = filterLength >= 8 && IsFlat(samples, filterLength, scale);
if (filterLength == 14 && filterEnabled && flat && IsOuterFlat(samples, scale))
{
ApplyWideReference(samples);
return;
}
if (filterLength >= 8 && filterEnabled && flat)
{
int p3 = samples[3];
int p2 = samples[4];
int p1 = samples[5];
int p0 = samples[6];
int q0 = samples[7];
int q1 = samples[8];
int q2 = samples[9];
int q3 = samples[10];
samples[4] = ((3 * p3) + (2 * p2) + p1 + p0 + q0 + 4) >> 3;
samples[5] = ((2 * p3) + p2 + (2 * p1) + p0 + q0 + q1 + 4) >> 3;
samples[6] = (p3 + p2 + p1 + (2 * p0) + q0 + q1 + q2 + 4) >> 3;
samples[7] = (p2 + p1 + p0 + (2 * q0) + q1 + q2 + q3 + 4) >> 3;
samples[8] = (p1 + p0 + q0 + (2 * q1) + q2 + (2 * q3) + 4) >> 3;
samples[9] = (p0 + q0 + q1 + (2 * q2) + (3 * q3) + 4) >> 3;
return;
}
ApplyNarrowReference(samples, filterEnabled, highEdgeVarianceThreshold * scale, bitDepth);
}
/// <summary>
/// Evaluates the AV1 filter mask for the selected reference width.
/// </summary>
private static bool IsFilterEnabled(ReadOnlySpan<int> samples, int filterLength, int limit, int boundaryLimit)
{
bool enabled = Math.Abs(samples[5] - samples[6]) <= limit
&& Math.Abs(samples[8] - samples[7]) <= limit
&& ((2 * Math.Abs(samples[6] - samples[7])) + (Math.Abs(samples[5] - samples[8]) >> 1)) <= boundaryLimit;
if (filterLength >= 6)
{
enabled = enabled
&& Math.Abs(samples[4] - samples[5]) <= limit
&& Math.Abs(samples[9] - samples[8]) <= limit;
}
if (filterLength >= 8)
{
enabled = enabled
&& Math.Abs(samples[3] - samples[4]) <= limit
&& Math.Abs(samples[10] - samples[9]) <= limit;
}
return enabled;
}
/// <summary>
/// Evaluates the AV1 inner flatness mask for the selected reference width.
/// </summary>
private static bool IsFlat(ReadOnlySpan<int> samples, int filterLength, int threshold)
{
bool flat = Math.Abs(samples[5] - samples[6]) <= threshold
&& Math.Abs(samples[8] - samples[7]) <= threshold
&& Math.Abs(samples[4] - samples[6]) <= threshold
&& Math.Abs(samples[9] - samples[7]) <= threshold;
return filterLength == 6
? flat
: flat && Math.Abs(samples[3] - samples[6]) <= threshold && Math.Abs(samples[10] - samples[7]) <= threshold;
}
/// <summary>
/// Evaluates the AV1 outer flatness mask for the fourteen-tap reference kernel.
/// </summary>
private static bool IsOuterFlat(ReadOnlySpan<int> samples, int threshold)
=> Math.Abs(samples[0] - samples[6]) <= threshold
&& Math.Abs(samples[1] - samples[6]) <= threshold
&& Math.Abs(samples[2] - samples[6]) <= threshold
&& Math.Abs(samples[11] - samples[7]) <= threshold
&& Math.Abs(samples[12] - samples[7]) <= threshold
&& Math.Abs(samples[13] - samples[7]) <= threshold;
/// <summary>
/// Applies the signed-saturating four-tap reference equations.
/// </summary>
private static void ApplyNarrowReference(Span<int> samples, bool filterEnabled, int highEdgeVarianceThreshold, int bitDepth)
{
if (!filterEnabled)
{
return;
}
int offset = 128 << (bitDepth - 8);
int minimum = -offset;
int maximum = offset - 1;
int p1 = samples[5] - offset;
int p0 = samples[6] - offset;
int q0 = samples[7] - offset;
int q1 = samples[8] - offset;
bool highVariance = Math.Abs(samples[5] - samples[6]) > highEdgeVarianceThreshold
|| Math.Abs(samples[8] - samples[7]) > highEdgeVarianceThreshold;
int filter = highVariance ? Math.Clamp(p1 - q1, minimum, maximum) : 0;
filter = Math.Clamp(filter + (3 * (q0 - p0)), minimum, maximum);
int filter1 = Math.Clamp(filter + 4, minimum, maximum) >> 3;
int filter2 = Math.Clamp(filter + 3, minimum, maximum) >> 3;
samples[7] = Math.Clamp(q0 - filter1, minimum, maximum) + offset;
samples[6] = Math.Clamp(p0 + filter2, minimum, maximum) + offset;
int outerFilter = highVariance ? 0 : (filter1 + 1) >> 1;
samples[8] = Math.Clamp(q1 - outerFilter, minimum, maximum) + offset;
samples[5] = Math.Clamp(p1 + outerFilter, minimum, maximum) + offset;
}
/// <summary>
/// Applies the thirteen-tap reference equations to the twelve modifiable samples.
/// </summary>
private static void ApplyWideReference(Span<int> samples)
{
int p6 = samples[0];
int p5 = samples[1];
int p4 = samples[2];
int p3 = samples[3];
int p2 = samples[4];
int p1 = samples[5];
int p0 = samples[6];
int q0 = samples[7];
int q1 = samples[8];
int q2 = samples[9];
int q3 = samples[10];
int q4 = samples[11];
int q5 = samples[12];
int q6 = samples[13];
samples[1] = ((7 * p6) + (2 * p5) + (2 * p4) + p3 + p2 + p1 + p0 + q0 + 8) >> 4;
samples[2] = ((5 * p6) + (2 * p5) + (2 * p4) + (2 * p3) + p2 + p1 + p0 + q0 + q1 + 8) >> 4;
samples[3] = ((4 * p6) + p5 + (2 * p4) + (2 * p3) + (2 * p2) + p1 + p0 + q0 + q1 + q2 + 8) >> 4;
samples[4] = ((3 * p6) + p5 + p4 + (2 * p3) + (2 * p2) + (2 * p1) + p0 + q0 + q1 + q2 + q3 + 8) >> 4;
samples[5] = ((2 * p6) + p5 + p4 + p3 + (2 * p2) + (2 * p1) + (2 * p0) + q0 + q1 + q2 + q3 + q4 + 8) >> 4;
samples[6] = (p6 + p5 + p4 + p3 + p2 + (2 * p1) + (2 * p0) + (2 * q0) + q1 + q2 + q3 + q4 + q5 + 8) >> 4;
samples[7] = (p5 + p4 + p3 + p2 + p1 + (2 * p0) + (2 * q0) + (2 * q1) + q2 + q3 + q4 + q5 + q6 + 8) >> 4;
samples[8] = (p4 + p3 + p2 + p1 + p0 + (2 * q0) + (2 * q1) + (2 * q2) + q3 + q4 + q5 + (2 * q6) + 8) >> 4;
samples[9] = (p3 + p2 + p1 + p0 + q0 + (2 * q1) + (2 * q2) + (2 * q3) + q4 + q5 + (3 * q6) + 8) >> 4;
samples[10] = (p2 + p1 + p0 + q0 + q1 + (2 * q2) + (2 * q3) + (2 * q4) + q5 + (4 * q6) + 8) >> 4;
samples[11] = (p1 + p0 + q0 + q1 + q2 + (2 * q3) + (2 * q4) + (2 * q5) + (5 * q6) + 8) >> 4;
samples[12] = (p0 + q0 + q1 + q2 + q3 + (2 * q4) + (2 * q5) + (7 * q6) + 8) >> 4;
}
}
Loading…
Cancel
Save