Browse Source

Implement SIMD-first AV1 CDEF filtering

pull/2633/head
James Jackson-South 7 days ago
parent
commit
23fec6cb3d
  1. 7
      HEIF_IMPLEMENTATION_PLAN.md
  2. 10
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs
  3. 1703
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.cs
  4. 321
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefKernels.cs
  5. 627
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CdefFilterTests.cs

7
HEIF_IMPLEMENTATION_PLAN.md

@ -268,7 +268,7 @@ This snapshot pins or classifies the available references and failures; it does
| `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. |
| `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. |
| `Av1CdefDecoder`, `Av1CdefFilter`, 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. |
| `Av1WienerFilter` | AV1 sections 7.17.4 and 7.17.5 Wiener restoration filtering and coefficient derivation | libaom `av1/common/restoration.c`, `av1/common/restoration.h`, `av1/common/convolve.c`, and `av1/common/convolve.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve the implicit center-sample contribution, separable horizontal/vertical rounding, bit-depth-dependent 16-bit intermediate range, and final 8/10/12-bit clipping. Reuse `Vector128_.MultiplyAddAdjacent` for the contiguous horizontal eight-tap product with an exact scalar fallback. Keep the restoration stage disabled until stripe boundaries and self-guided filtering are both complete. |
@ -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 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. Exact native-plane comparison with pinned scalar libaom output now verifies active deblocking and complete reconstruction for real 8-bit 4:2:0, 10-bit 4:4:4, and 12-bit 4:4:4 content; genuine AVIF containers separately verify presentation and public bit-depth metadata. The pipeline 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 vectors exercising CDEF, super-resolution, restoration, and grain remain required before those stages 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. Exact native-plane comparison with pinned scalar libaom output now verifies active deblocking and complete reconstruction for real 8-bit 4:2:0, 10-bit 4:4:4, and 12-bit 4:4:4 content; genuine AVIF containers separately verify presentation and public bit-depth metadata. The pipeline then applies CDEF through one semantic filter architecture: paired AVX2 and single-block `Vector128` direction analysis, closed primary/secondary strength operators, packed 4x4/4x8/8x4/8x8 constrained filtering, byte/16-bit output operators, and an exact allocation-free scalar fallback. The decoder still uses heap-allocated immutable plane snapshots and direction/variance maps; moving those to allocator-owned memory and using paired direction traversal is the next CDEF step. 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 vectors exercising CDEF, super-resolution, restoration, and grain remain required before those stages 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.
@ -505,6 +505,9 @@ Implement and verify in dependency order:
- [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.
- [x] Verify deblocking syntax, filter-level derivation, and boundary traversal with independently encoded 8/10/12-bit AV1 samples and exact scalar-libaom planes; verify presented reconstruction and public precision with genuine AVIF containers at every supported bit depth.
- [ ] CDEF.
- [x] Replace the scalar helper bucket with an allocation-free SIMD-first `Av1CdefFilter`: vector direction analysis, 4x4/4x8/8x4/8x8 constrained filtering, byte/16-bit output operators, and one exact scalar fallback verified through `FeatureTestRunner`.
- [ ] Move the immutable deblocked-plane snapshot and frame-wide direction/variance maps to allocator-owned decoder memory, preserving luma-first direction derivation, skipped-block selection, asymmetric chroma direction conversion, and frame-edge sentinels.
- [ ] Verify CDEF syntax, strength derivation, unit/block traversal, subsampling, frame edges, and presented reconstruction with independently encoded active-CDEF AV1/AVIF fixtures across 8/10/12-bit samples.
- [ ] Super-resolution scaling.
- [ ] Loop restoration.
- [ ] Frame padding and film-grain synthesis when signaled.

10
src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs

@ -130,7 +130,7 @@ internal class Av1CdefDecoder
// CDEF output must never become input to a later block. The sentinel border also makes
// frame-edge taps follow AV1 without exposing the frame buffer's prediction padding.
ushort[] source = new ushort[(planeHeight + (SourceBorder * 2)) * sourceStride];
Array.Fill(source, Av1CdefKernels.VeryLarge);
Array.Fill(source, Av1CdefFilter.VeryLarge);
Span<byte> lowBitDepthDestination = default;
Span<ushort> highBitDepthDestination = default;
@ -236,7 +236,7 @@ internal class Av1CdefDecoder
if (plane == Av1Plane.Y)
{
directions[directionIndex] = Av1CdefKernels.FindDirection(
directions[directionIndex] = Av1CdefFilter.FindDirection(
source,
sourceOffset,
sourceStride,
@ -250,7 +250,7 @@ internal class Av1CdefDecoder
}
int filteredPrimaryStrength = plane == Av1Plane.Y
? Av1CdefKernels.AdjustStrength(primaryStrength, variances[directionIndex])
? Av1CdefFilter.AdjustStrength(primaryStrength, variances[directionIndex])
: primaryStrength;
if (filteredPrimaryStrength == 0 && secondaryStrength == 0)
@ -261,10 +261,10 @@ internal class Av1CdefDecoder
// Secondary-only filtering uses direction zero; otherwise chroma remaps the
// luma direction into its asymmetrically subsampled sample grid when required.
int direction = primaryStrength != 0
? Av1CdefKernels.ConvertDirection(directions[directionIndex], subsamplingX, subsamplingY)
? Av1CdefFilter.ConvertDirection(directions[directionIndex], subsamplingX, subsamplingY)
: 0;
Av1CdefKernels.FilterBlock(
Av1CdefFilter.FilterBlock(
source,
sourceOffset,
sourceStride,

1703
src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.cs

File diff suppressed because it is too large

321
src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefKernels.cs

@ -1,321 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
/// <summary>
/// Provides the scalar constrained directional enhancement filter operations defined by AV1.
/// </summary>
internal static class Av1CdefKernels
{
/// <summary>
/// The sample value used for neighbors outside the coded frame.
/// </summary>
public const ushort VeryLarge = 0x4000;
/// <summary>
/// The horizontal offsets for the nearest primary or secondary taps in each direction.
/// </summary>
private static readonly int[] DirectionX1 = [1, 1, 1, 1, 1, 0, 0, 0];
/// <summary>
/// The vertical offsets for the nearest primary or secondary taps in each direction.
/// </summary>
private static readonly int[] DirectionY1 = [-1, 0, 0, 0, 1, 1, 1, 1];
/// <summary>
/// The horizontal offsets for the furthest primary or secondary taps in each direction.
/// </summary>
private static readonly int[] DirectionX2 = [2, 2, 2, 2, 2, 1, 0, -1];
/// <summary>
/// The vertical offsets for the furthest primary or secondary taps in each direction.
/// </summary>
private static readonly int[] DirectionY2 = [-2, -1, 0, 1, 2, 2, 2, 2];
/// <summary>
/// The primary-tap weights selected by the parity of the unscaled primary strength.
/// </summary>
private static readonly int[,] PrimaryTaps =
{
{ 4, 2 },
{ 3, 3 }
};
/// <summary>
/// The secondary-tap weights for the nearest and furthest samples.
/// </summary>
private static readonly int[] SecondaryTaps = [2, 1];
/// <summary>
/// The common multiples used to compare line variance without division.
/// </summary>
private static readonly int[] DivisionTable = [0, 840, 420, 280, 210, 168, 140, 120, 105];
/// <summary>
/// The direction mapping for horizontally subsampled, vertically full-resolution chroma.
/// </summary>
private static readonly int[] DirectionMap422 = [7, 0, 2, 4, 5, 6, 6, 6];
/// <summary>
/// The direction mapping for horizontally full-resolution, vertically subsampled chroma.
/// </summary>
private static readonly int[] DirectionMap440 = [1, 2, 2, 2, 3, 4, 6, 0];
/// <summary>
/// Finds the dominant direction of an 8x8 luma block and its directional variance.
/// </summary>
/// <param name="source">The bordered, deblocked source plane.</param>
/// <param name="sourceOffset">The offset of the block's top-left sample.</param>
/// <param name="sourceStride">The number of samples between adjacent source rows.</param>
/// <param name="coefficientShift">The number of bits above the eight-bit analysis precision.</param>
/// <param name="variance">Receives the variance difference between the selected and orthogonal directions.</param>
/// <returns>The zero-based AV1 direction index.</returns>
public static int FindDirection(
ReadOnlySpan<ushort> source,
int sourceOffset,
int sourceStride,
int coefficientShift,
out int variance)
{
Span<int> partial = stackalloc int[8 * 15];
Span<int> cost = stackalloc int[8];
partial.Clear();
cost.Clear();
for (int row = 0; row < 8; row++)
{
for (int column = 0; column < 8; column++)
{
// Direction analysis deliberately reduces every source to eight-bit precision so its
// strength selection is identical for 8-, 10-, and 12-bit coded images.
int value = (source[sourceOffset + (row * sourceStride) + column] >> coefficientShift) - 128;
partial[(0 * 15) + row + column] += value;
partial[(1 * 15) + row + (column / 2)] += value;
partial[(2 * 15) + row] += value;
partial[(3 * 15) + 3 + row - (column / 2)] += value;
partial[(4 * 15) + 7 + row - column] += value;
partial[(5 * 15) + 3 - (row / 2) + column] += value;
partial[(6 * 15) + column] += value;
partial[(7 * 15) + (row / 2) + column] += value;
}
}
for (int i = 0; i < 8; i++)
{
cost[2] += partial[(2 * 15) + i] * partial[(2 * 15) + i];
cost[6] += partial[(6 * 15) + i] * partial[(6 * 15) + i];
}
cost[2] *= DivisionTable[8];
cost[6] *= DivisionTable[8];
for (int i = 0; i < 7; i++)
{
cost[0] += ((partial[(0 * 15) + i] * partial[(0 * 15) + i]) +
(partial[(0 * 15) + 14 - i] * partial[(0 * 15) + 14 - i])) * DivisionTable[i + 1];
cost[4] += ((partial[(4 * 15) + i] * partial[(4 * 15) + i]) +
(partial[(4 * 15) + 14 - i] * partial[(4 * 15) + 14 - i])) * DivisionTable[i + 1];
}
cost[0] += partial[(0 * 15) + 7] * partial[(0 * 15) + 7] * DivisionTable[8];
cost[4] += partial[(4 * 15) + 7] * partial[(4 * 15) + 7] * DivisionTable[8];
for (int direction = 1; direction < 8; direction += 2)
{
for (int i = 0; i < 5; i++)
{
cost[direction] += partial[(direction * 15) + 3 + i] * partial[(direction * 15) + 3 + i];
}
cost[direction] *= DivisionTable[8];
for (int i = 0; i < 3; i++)
{
cost[direction] += ((partial[(direction * 15) + i] * partial[(direction * 15) + i]) +
(partial[(direction * 15) + 10 - i] * partial[(direction * 15) + 10 - i])) * DivisionTable[(2 * i) + 2];
}
}
int bestCost = 0;
int bestDirection = 0;
for (int direction = 0; direction < 8; direction++)
{
if (cost[direction] > bestCost)
{
bestCost = cost[direction];
bestDirection = direction;
}
}
// Both costs omit the same sum-of-squares term. Their scaled difference is the
// directional variance consumed by AV1's luma strength adjustment.
variance = (bestCost - cost[(bestDirection + 4) & 7]) >> 10;
return bestDirection;
}
/// <summary>
/// Adjusts a luma primary strength according to the directional variance of its 8x8 block.
/// </summary>
/// <param name="strength">The bit-depth-scaled primary strength.</param>
/// <param name="variance">The directional variance returned by <see cref="FindDirection"/>.</param>
/// <returns>The variance-adjusted primary strength.</returns>
public static int AdjustStrength(int strength, int variance)
{
int varianceClass = variance >> 6;
int adjustment = varianceClass != 0 ? Math.Min(Av1Math.MostSignificantBit((uint)varianceClass), 12) : 0;
return variance != 0 ? ((strength * (4 + adjustment)) + 8) >> 4 : 0;
}
/// <summary>
/// Converts a luma direction to the matching chroma direction for asymmetric subsampling.
/// </summary>
/// <param name="direction">The zero-based luma direction index.</param>
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
/// <param name="subsamplingY">The vertical chroma subsampling shift.</param>
/// <returns>The direction index in the chroma sample grid.</returns>
public static int ConvertDirection(int direction, int subsamplingX, int subsamplingY)
{
if (subsamplingX == subsamplingY)
{
return direction;
}
return subsamplingX != 0 ? DirectionMap422[direction] : DirectionMap440[direction];
}
/// <summary>
/// Filters one luma or chroma block from an immutable bordered source plane.
/// </summary>
/// <param name="source">The bordered, deblocked source plane.</param>
/// <param name="sourceOffset">The offset of the block's top-left source sample.</param>
/// <param name="sourceStride">The number of samples between adjacent source rows.</param>
/// <param name="destination">The unbordered filtered destination plane.</param>
/// <param name="destinationOffset">The offset of the block's top-left destination sample.</param>
/// <param name="destinationStride">The number of samples between adjacent destination rows.</param>
/// <param name="primaryStrength">The bit-depth-scaled primary strength.</param>
/// <param name="secondaryStrength">The bit-depth-scaled secondary strength.</param>
/// <param name="direction">The zero-based AV1 direction index.</param>
/// <param name="primaryDamping">The damping value applied to primary taps.</param>
/// <param name="secondaryDamping">The damping value applied to secondary taps.</param>
/// <param name="coefficientShift">The number of bits above eight-bit sample precision.</param>
/// <param name="blockWidth">The block width in plane samples.</param>
/// <param name="blockHeight">The block height in plane samples.</param>
public static void FilterBlock(
ReadOnlySpan<ushort> source,
int sourceOffset,
int sourceStride,
Span<ushort> destination,
int destinationOffset,
int destinationStride,
int primaryStrength,
int secondaryStrength,
int direction,
int primaryDamping,
int secondaryDamping,
int coefficientShift,
int blockWidth,
int blockHeight)
{
bool enablePrimary = primaryStrength != 0;
bool enableSecondary = secondaryStrength != 0;
bool clippingRequired = enablePrimary && enableSecondary;
int primaryTapSet = (primaryStrength >> coefficientShift) & 1;
for (int row = 0; row < blockHeight; row++)
{
for (int column = 0; column < blockWidth; column++)
{
int sourceIndex = sourceOffset + (row * sourceStride) + column;
int sample = source[sourceIndex];
int sum = 0;
int minimum = sample;
int maximum = sample;
for (int tap = 0; tap < 2; tap++)
{
if (enablePrimary)
{
int primaryDirectionOffset = GetDirectionOffset(direction, tap, sourceStride);
int neighbor0 = source[sourceIndex + primaryDirectionOffset];
int neighbor1 = source[sourceIndex - primaryDirectionOffset];
int weight = PrimaryTaps[primaryTapSet, tap];
sum += weight * Constrain(neighbor0 - sample, primaryStrength, primaryDamping);
sum += weight * Constrain(neighbor1 - sample, primaryStrength, primaryDamping);
if (clippingRequired)
{
maximum = neighbor0 != VeryLarge ? Math.Max(maximum, neighbor0) : maximum;
maximum = neighbor1 != VeryLarge ? Math.Max(maximum, neighbor1) : maximum;
minimum = Math.Min(minimum, neighbor0);
minimum = Math.Min(minimum, neighbor1);
}
}
if (enableSecondary)
{
int secondaryDirection0 = GetDirectionOffset((direction + 2) & 7, tap, sourceStride);
int secondaryDirection1 = GetDirectionOffset((direction + 6) & 7, tap, sourceStride);
int neighbor0 = source[sourceIndex + secondaryDirection0];
int neighbor1 = source[sourceIndex - secondaryDirection0];
int neighbor2 = source[sourceIndex + secondaryDirection1];
int neighbor3 = source[sourceIndex - secondaryDirection1];
int weight = SecondaryTaps[tap];
if (clippingRequired)
{
maximum = neighbor0 != VeryLarge ? Math.Max(maximum, neighbor0) : maximum;
maximum = neighbor1 != VeryLarge ? Math.Max(maximum, neighbor1) : maximum;
maximum = neighbor2 != VeryLarge ? Math.Max(maximum, neighbor2) : maximum;
maximum = neighbor3 != VeryLarge ? Math.Max(maximum, neighbor3) : maximum;
minimum = Math.Min(minimum, neighbor0);
minimum = Math.Min(minimum, neighbor1);
minimum = Math.Min(minimum, neighbor2);
minimum = Math.Min(minimum, neighbor3);
}
sum += weight * Constrain(neighbor0 - sample, secondaryStrength, secondaryDamping);
sum += weight * Constrain(neighbor1 - sample, secondaryStrength, secondaryDamping);
sum += weight * Constrain(neighbor2 - sample, secondaryStrength, secondaryDamping);
sum += weight * Constrain(neighbor3 - sample, secondaryStrength, secondaryDamping);
}
}
// The negative-sum correction preserves AV1's asymmetric signed rounding exactly.
int filtered = sample + ((8 + sum - (sum < 0 ? 1 : 0)) >> 4);
destination[destinationOffset + (row * destinationStride) + column] =
(ushort)(clippingRequired ? Av1Math.Clip3(minimum, maximum, filtered) : filtered);
}
}
}
/// <summary>
/// Converts a direction and tap number to a signed plane-buffer offset.
/// </summary>
/// <param name="direction">The zero-based AV1 direction index.</param>
/// <param name="tap">The zero-based distance index.</param>
/// <param name="stride">The number of samples between adjacent rows.</param>
/// <returns>The signed sample offset.</returns>
private static int GetDirectionOffset(int direction, int tap, int stride)
=> tap == 0
? (DirectionY1[direction] * stride) + DirectionX1[direction]
: (DirectionY2[direction] * stride) + DirectionX2[direction];
/// <summary>
/// Limits a neighbor difference according to a filter strength and damping value.
/// </summary>
/// <param name="difference">The signed difference from the current sample.</param>
/// <param name="threshold">The bit-depth-scaled filter strength.</param>
/// <param name="damping">The damping value.</param>
/// <returns>The signed constrained difference.</returns>
private static int Constrain(int difference, int threshold, int damping)
{
if (threshold == 0)
{
return 0;
}
// Stronger thresholds reduce the effective damping shift, matching the AV1 constrain function.
int shift = Math.Max(0, damping - Av1Math.MostSignificantBit((uint)threshold));
int magnitude = Math.Abs(difference);
int constrained = Av1Math.Clip3(0, magnitude, threshold - (magnitude >> shift));
return difference < 0 ? -constrained : constrained;
}
}

627
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CdefFilterTests.cs

@ -0,0 +1,627 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
/// <summary>
/// Verifies AV1 constrained directional enhancement filtering across sample precision, block geometry, and intrinsic tiers.
/// </summary>
[Trait("Format", "Avif")]
public class Av1CdefFilterTests
{
/// <summary>
/// The hardware configurations required to exercise packed filtering and the scalar fallback.
/// </summary>
private const HwIntrinsics Configurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic;
/// <summary>
/// The row stride of the bordered source plane used by the filter tests.
/// </summary>
private const int SourceStride = 16;
/// <summary>
/// The number of unavailable samples surrounding the test image.
/// </summary>
private const int SourceBorder = 2;
/// <summary>
/// Verifies direction selection and variance against an independent scalar definition.
/// </summary>
[Fact]
public void FindDirectionMatchesIndependentDefinitionAcrossIntrinsicTiers()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateDirections, Configurations);
/// <summary>
/// Verifies every CDEF block geometry and strength mode against an independent scalar definition.
/// </summary>
[Fact]
public void FilterBlockMatchesIndependentDefinitionAcrossIntrinsicTiers()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateFilters, Configurations);
/// <summary>
/// Verifies the complete asymmetric chroma direction mappings and the unchanged symmetric mappings.
/// </summary>
[Fact]
public void ConvertDirectionMatchesSubsamplingGeometry()
{
int[] horizontalSubsampling = [7, 0, 2, 4, 5, 6, 6, 6];
int[] verticalSubsampling = [1, 2, 2, 2, 3, 4, 6, 0];
for (int direction = 0; direction < 8; direction++)
{
Assert.Equal(horizontalSubsampling[direction], Av1CdefFilter.ConvertDirection(direction, 1, 0));
Assert.Equal(verticalSubsampling[direction], Av1CdefFilter.ConvertDirection(direction, 0, 1));
Assert.Equal(direction, Av1CdefFilter.ConvertDirection(direction, 0, 0));
Assert.Equal(direction, Av1CdefFilter.ConvertDirection(direction, 1, 1));
}
}
/// <summary>
/// Verifies luma strength adjustment at zero, logarithmic-class boundaries, and the capped variance class.
/// </summary>
[Fact]
public void AdjustStrengthMatchesIndependentDefinition()
{
foreach (int strength in new[] { 0, 4, 15, 60 })
{
foreach (int variance in new[] { 0, 1, 63, 64, 255, 4096, 1 << 20 })
{
int varianceClass = variance >> 6;
int adjustment = varianceClass == 0 ? 0 : Math.Min(BitOperations.Log2((uint)varianceClass), 12);
int expected = variance == 0 ? 0 : ((strength * (4 + adjustment)) + 8) >> 4;
Assert.Equal(expected, Av1CdefFilter.AdjustStrength(strength, variance));
}
}
}
/// <summary>
/// Verifies that the deterministic direction corpus exercises every selected-direction branch.
/// </summary>
[Fact]
public void DirectionCorpusCoversEveryDirection()
{
const int stride = 32;
const int sourceOffset = (4 * stride) + 5;
int secondSourceOffset = sourceOffset + 8;
HashSet<int> observedDirections = [];
for (int pattern = 0; pattern < 8; pattern++)
{
ushort[] source = new ushort[stride * 16];
PopulateDirectionSource(source, sourceOffset, stride, pattern, 8);
observedDirections.Add(FindDirectionReference(source, sourceOffset, stride, 0, out _));
observedDirections.Add(FindDirectionReference(source, secondSourceOffset, stride, 0, out _));
}
Assert.Equal(Enumerable.Range(0, 8), observedDirections.Order());
}
/// <summary>
/// Exercises direction search with multiple source patterns at every supported sample precision.
/// </summary>
private static void ValidateDirections()
{
const int stride = 32;
const int sourceOffset = (4 * stride) + 5;
foreach (int bitDepth in new[] { 8, 10, 12 })
{
int coefficientShift = bitDepth - 8;
for (int pattern = 0; pattern < 8; pattern++)
{
ushort[] source = new ushort[stride * 16];
int secondSourceOffset = sourceOffset + 8;
PopulateDirectionSource(source, sourceOffset, stride, pattern, bitDepth);
int expectedDirection = FindDirectionReference(source, sourceOffset, stride, coefficientShift, out int expectedVariance);
int actualDirection = Av1CdefFilter.FindDirection(source, sourceOffset, stride, coefficientShift, out int actualVariance);
Assert.Equal(expectedDirection, actualDirection);
Assert.Equal(expectedVariance, actualVariance);
int secondExpectedDirection = FindDirectionReference(source, secondSourceOffset, stride, coefficientShift, out int secondExpectedVariance);
Av1CdefFilter.FindDirections(
source,
sourceOffset,
secondSourceOffset,
stride,
coefficientShift,
out int firstActualDirection,
out int firstActualVariance,
out int secondActualDirection,
out int secondActualVariance);
Assert.Equal(expectedDirection, firstActualDirection);
Assert.Equal(expectedVariance, firstActualVariance);
Assert.Equal(secondExpectedDirection, secondActualDirection);
Assert.Equal(secondExpectedVariance, secondActualVariance);
}
}
}
/// <summary>
/// Populates two adjacent 8x8 blocks with deterministic directional samples.
/// </summary>
/// <param name="source">The destination source plane.</param>
/// <param name="sourceOffset">The first populated sample.</param>
/// <param name="sourceStride">The source row stride.</param>
/// <param name="pattern">The deterministic pattern index.</param>
/// <param name="bitDepth">The sample precision.</param>
private static void PopulateDirectionSource(Span<ushort> source, int sourceOffset, int sourceStride, int pattern, int bitDepth)
{
int coefficientShift = bitDepth - 8;
int maximum = (1 << bitDepth) - 1;
for (int row = 0; row < 8; row++)
{
for (int column = 0; column < 16; column++)
{
int localColumn = column & 7;
int direction = column < 8 ? pattern : (pattern + 4) & 7;
int line = GetDirectionLineReference(direction, row, localColumn);
// Samples are constant along the requested geometric line and vary between lines. Direction search
// therefore minimizes reconstruction error in that direction while still exercising nonuniform values.
int value = (24 + (line * 14)) << coefficientShift;
source[sourceOffset + (row * sourceStride) + column] = (ushort)(value & maximum);
}
}
}
/// <summary>
/// Maps one source coordinate to its line in an AV1 direction independently of the production implementation.
/// </summary>
/// <param name="direction">The zero-based AV1 direction index.</param>
/// <param name="row">The source row.</param>
/// <param name="column">The source column.</param>
/// <returns>The zero-based line index.</returns>
private static int GetDirectionLineReference(int direction, int row, int column) => direction switch
{
0 => row + column,
1 => row + (column / 2),
2 => row,
3 => 3 + row - (column / 2),
4 => 7 + row - column,
5 => 3 - (row / 2) + column,
6 => column,
_ => (row / 2) + column
};
/// <summary>
/// Exercises all normative block dimensions, directions, strength combinations, output types, and coded precisions.
/// </summary>
private static void ValidateFilters()
{
(int Width, int Height)[] dimensions = [(4, 4), (4, 8), (8, 4), (8, 8)];
foreach (int bitDepth in new[] { 8, 10, 12 })
{
int coefficientShift = bitDepth - 8;
int scale = 1 << coefficientShift;
ushort[] source = CreateBorderedSource(bitDepth);
int sourceOffset = (SourceBorder * SourceStride) + SourceBorder;
(int Primary, int Secondary)[] strengths = [(0, 0), (4 * scale, 0), (0, 2 * scale), (5 * scale, 2 * scale)];
foreach ((int blockWidth, int blockHeight) in dimensions)
{
foreach (int direction in Enumerable.Range(0, 8))
{
foreach ((int primaryStrength, int secondaryStrength) in strengths)
{
if (bitDepth == 8)
{
AssertByteFilter(
source,
sourceOffset,
blockWidth,
blockHeight,
primaryStrength,
secondaryStrength,
direction,
coefficientShift);
}
AssertUInt16Filter(
source,
sourceOffset,
blockWidth,
blockHeight,
primaryStrength,
secondaryStrength,
direction,
coefficientShift);
}
}
}
}
}
/// <summary>
/// Creates a deterministic image whose top-left output block touches the unavailable-neighbor border.
/// </summary>
/// <param name="bitDepth">The source sample precision.</param>
/// <returns>The bordered 16-bit source plane.</returns>
private static ushort[] CreateBorderedSource(int bitDepth)
{
ushort[] source = Enumerable.Repeat(Av1CdefFilter.VeryLarge, SourceStride * SourceStride).ToArray();
int maximum = (1 << bitDepth) - 1;
int scale = 1 << (bitDepth - 8);
for (int row = SourceBorder; row < SourceStride - SourceBorder; row++)
{
for (int column = SourceBorder; column < SourceStride - SourceBorder; column++)
{
int localRow = row - SourceBorder;
int localColumn = column - SourceBorder;
int value = (72 + (localRow * 9) + (localColumn * 5) + ((localRow * localColumn) & 15)) * scale;
source[(row * SourceStride) + column] = (ushort)Math.Min(value, maximum);
}
}
return source;
}
/// <summary>
/// Verifies one eight-bit output block while retaining untouched destination padding in the comparison.
/// </summary>
/// <param name="source">The bordered source plane.</param>
/// <param name="sourceOffset">The first source sample in the block.</param>
/// <param name="blockWidth">The output block width.</param>
/// <param name="blockHeight">The output block height.</param>
/// <param name="primaryStrength">The primary filter strength.</param>
/// <param name="secondaryStrength">The secondary filter strength.</param>
/// <param name="direction">The primary filter direction.</param>
/// <param name="coefficientShift">The source precision shift.</param>
private static void AssertByteFilter(
ushort[] source,
int sourceOffset,
int blockWidth,
int blockHeight,
int primaryStrength,
int secondaryStrength,
int direction,
int coefficientShift)
{
const int destinationStride = 12;
const int destinationOffset = destinationStride + 1;
byte[] expected = Enumerable.Repeat((byte)231, destinationStride * 10).ToArray();
byte[] actual = (byte[])expected.Clone();
int damping = 5 + coefficientShift;
FilterReference(
source,
sourceOffset,
SourceStride,
expected,
destinationOffset,
destinationStride,
primaryStrength,
secondaryStrength,
direction,
damping,
coefficientShift,
blockWidth,
blockHeight);
Av1CdefFilter.FilterBlock(
source,
sourceOffset,
SourceStride,
actual,
destinationOffset,
destinationStride,
primaryStrength,
secondaryStrength,
direction,
damping,
damping,
coefficientShift,
blockWidth,
blockHeight);
Assert.Equal(expected, actual);
}
/// <summary>
/// Verifies one 16-bit output block while retaining untouched destination padding in the comparison.
/// </summary>
/// <param name="source">The bordered source plane.</param>
/// <param name="sourceOffset">The first source sample in the block.</param>
/// <param name="blockWidth">The output block width.</param>
/// <param name="blockHeight">The output block height.</param>
/// <param name="primaryStrength">The primary filter strength.</param>
/// <param name="secondaryStrength">The secondary filter strength.</param>
/// <param name="direction">The primary filter direction.</param>
/// <param name="coefficientShift">The source precision shift.</param>
private static void AssertUInt16Filter(
ushort[] source,
int sourceOffset,
int blockWidth,
int blockHeight,
int primaryStrength,
int secondaryStrength,
int direction,
int coefficientShift)
{
const int destinationStride = 12;
const int destinationOffset = destinationStride + 1;
ushort[] expected = Enumerable.Repeat((ushort)60000, destinationStride * 10).ToArray();
ushort[] actual = (ushort[])expected.Clone();
int damping = 5 + coefficientShift;
FilterReference(
source,
sourceOffset,
SourceStride,
expected,
destinationOffset,
destinationStride,
primaryStrength,
secondaryStrength,
direction,
damping,
coefficientShift,
blockWidth,
blockHeight);
Av1CdefFilter.FilterBlock(
source,
sourceOffset,
SourceStride,
actual,
destinationOffset,
destinationStride,
primaryStrength,
secondaryStrength,
direction,
damping,
damping,
coefficientShift,
blockWidth,
blockHeight);
Assert.Equal(expected, actual);
}
/// <summary>
/// Finds one direction and variance using the scalar AV1 definition independently of the production layouts.
/// </summary>
/// <param name="source">The source plane.</param>
/// <param name="sourceOffset">The first sample in the 8x8 block.</param>
/// <param name="sourceStride">The source row stride.</param>
/// <param name="coefficientShift">The source precision shift.</param>
/// <param name="variance">Receives the directional variance.</param>
/// <returns>The selected direction.</returns>
private static int FindDirectionReference(
ReadOnlySpan<ushort> source,
int sourceOffset,
int sourceStride,
int coefficientShift,
out int variance)
{
int[,] partials = new int[8, 15];
int[] costs = new int[8];
int[] divisions = [0, 840, 420, 280, 210, 168, 140, 120, 105];
for (int row = 0; row < 8; row++)
{
for (int column = 0; column < 8; column++)
{
int value = (source[sourceOffset + (row * sourceStride) + column] >> coefficientShift) - 128;
partials[0, row + column] += value;
partials[1, row + (column / 2)] += value;
partials[2, row] += value;
partials[3, 3 + row - (column / 2)] += value;
partials[4, 7 + row - column] += value;
partials[5, 3 - (row / 2) + column] += value;
partials[6, column] += value;
partials[7, (row / 2) + column] += value;
}
}
for (int line = 0; line < 8; line++)
{
costs[2] += partials[2, line] * partials[2, line];
costs[6] += partials[6, line] * partials[6, line];
}
costs[2] *= divisions[8];
costs[6] *= divisions[8];
for (int line = 0; line < 7; line++)
{
costs[0] += ((partials[0, line] * partials[0, line]) + (partials[0, 14 - line] * partials[0, 14 - line])) * divisions[line + 1];
costs[4] += ((partials[4, line] * partials[4, line]) + (partials[4, 14 - line] * partials[4, 14 - line])) * divisions[line + 1];
}
costs[0] += partials[0, 7] * partials[0, 7] * divisions[8];
costs[4] += partials[4, 7] * partials[4, 7] * divisions[8];
for (int direction = 1; direction < 8; direction += 2)
{
for (int line = 0; line < 5; line++)
{
costs[direction] += partials[direction, 3 + line] * partials[direction, 3 + line];
}
costs[direction] *= divisions[8];
for (int line = 0; line < 3; line++)
{
costs[direction] += ((partials[direction, line] * partials[direction, line])
+ (partials[direction, 10 - line] * partials[direction, 10 - line])) * divisions[(2 * line) + 2];
}
}
int bestCost = 0;
int bestDirection = 0;
for (int direction = 0; direction < 8; direction++)
{
if (costs[direction] > bestCost)
{
bestCost = costs[direction];
bestDirection = direction;
}
}
variance = (bestCost - costs[(bestDirection + 4) & 7]) >> 10;
return bestDirection;
}
/// <summary>
/// Applies the independent scalar filter definition to eight-bit output storage.
/// </summary>
private static void FilterReference(
ReadOnlySpan<ushort> source,
int sourceOffset,
int sourceStride,
Span<byte> destination,
int destinationOffset,
int destinationStride,
int primaryStrength,
int secondaryStrength,
int direction,
int damping,
int coefficientShift,
int blockWidth,
int blockHeight)
{
for (int row = 0; row < blockHeight; row++)
{
for (int column = 0; column < blockWidth; column++)
{
destination[destinationOffset + (row * destinationStride) + column] = (byte)FilterSampleReference(
source,
sourceOffset + (row * sourceStride) + column,
sourceStride,
primaryStrength,
secondaryStrength,
direction,
damping,
coefficientShift);
}
}
}
/// <summary>
/// Applies the independent scalar filter definition to 16-bit output storage.
/// </summary>
private static void FilterReference(
ReadOnlySpan<ushort> source,
int sourceOffset,
int sourceStride,
Span<ushort> destination,
int destinationOffset,
int destinationStride,
int primaryStrength,
int secondaryStrength,
int direction,
int damping,
int coefficientShift,
int blockWidth,
int blockHeight)
{
for (int row = 0; row < blockHeight; row++)
{
for (int column = 0; column < blockWidth; column++)
{
destination[destinationOffset + (row * destinationStride) + column] = (ushort)FilterSampleReference(
source,
sourceOffset + (row * sourceStride) + column,
sourceStride,
primaryStrength,
secondaryStrength,
direction,
damping,
coefficientShift);
}
}
}
/// <summary>
/// Computes one independently filtered sample from its primary and secondary neighbors.
/// </summary>
private static int FilterSampleReference(
ReadOnlySpan<ushort> source,
int sourceIndex,
int sourceStride,
int primaryStrength,
int secondaryStrength,
int direction,
int damping,
int coefficientShift)
{
bool enablePrimary = primaryStrength != 0;
bool enableSecondary = secondaryStrength != 0;
bool clippingRequired = enablePrimary && enableSecondary;
int primaryTapSet = (primaryStrength >> coefficientShift) & 1;
int sample = source[sourceIndex];
int sum = 0;
int minimum = sample;
int maximum = sample;
for (int tap = 0; tap < 2; tap++)
{
if (enablePrimary)
{
int offset = GetDirectionOffsetReference(direction, tap, sourceStride);
int neighbor0 = source[sourceIndex + offset];
int neighbor1 = source[sourceIndex - offset];
int weight = primaryTapSet == 0 ? (tap == 0 ? 4 : 2) : 3;
sum += weight * ConstrainReference(neighbor0 - sample, primaryStrength, damping);
sum += weight * ConstrainReference(neighbor1 - sample, primaryStrength, damping);
if (clippingRequired)
{
maximum = neighbor0 != Av1CdefFilter.VeryLarge ? Math.Max(maximum, neighbor0) : maximum;
maximum = neighbor1 != Av1CdefFilter.VeryLarge ? Math.Max(maximum, neighbor1) : maximum;
minimum = Math.Min(minimum, Math.Min(neighbor0, neighbor1));
}
}
if (enableSecondary)
{
int offset0 = GetDirectionOffsetReference((direction + 2) & 7, tap, sourceStride);
int offset1 = GetDirectionOffsetReference((direction + 6) & 7, tap, sourceStride);
int neighbor0 = source[sourceIndex + offset0];
int neighbor1 = source[sourceIndex - offset0];
int neighbor2 = source[sourceIndex + offset1];
int neighbor3 = source[sourceIndex - offset1];
int weight = tap == 0 ? 2 : 1;
sum += weight * ConstrainReference(neighbor0 - sample, secondaryStrength, damping);
sum += weight * ConstrainReference(neighbor1 - sample, secondaryStrength, damping);
sum += weight * ConstrainReference(neighbor2 - sample, secondaryStrength, damping);
sum += weight * ConstrainReference(neighbor3 - sample, secondaryStrength, damping);
if (clippingRequired)
{
maximum = neighbor0 != Av1CdefFilter.VeryLarge ? Math.Max(maximum, neighbor0) : maximum;
maximum = neighbor1 != Av1CdefFilter.VeryLarge ? Math.Max(maximum, neighbor1) : maximum;
maximum = neighbor2 != Av1CdefFilter.VeryLarge ? Math.Max(maximum, neighbor2) : maximum;
maximum = neighbor3 != Av1CdefFilter.VeryLarge ? Math.Max(maximum, neighbor3) : maximum;
minimum = Math.Min(minimum, Math.Min(Math.Min(neighbor0, neighbor1), Math.Min(neighbor2, neighbor3)));
}
}
}
int filtered = sample + ((8 + sum - (sum < 0 ? 1 : 0)) >> 4);
return clippingRequired ? Math.Clamp(filtered, minimum, maximum) : filtered;
}
/// <summary>
/// Applies the scalar AV1 constrain equation independently of the production implementation.
/// </summary>
private static int ConstrainReference(int difference, int threshold, int damping)
{
int shift = Math.Max(0, damping - BitOperations.Log2((uint)threshold));
int magnitude = Math.Abs(difference);
int constrained = Math.Clamp(threshold - (magnitude >> shift), 0, magnitude);
return difference < 0 ? -constrained : constrained;
}
/// <summary>
/// Converts a direction and tap to a signed source offset independently of the production implementation.
/// </summary>
private static int GetDirectionOffsetReference(int direction, int tap, int stride)
{
int[] x0 = [1, 1, 1, 1, 1, 0, 0, 0];
int[] y0 = [-1, 0, 0, 0, 1, 1, 1, 1];
int[] x1 = [2, 2, 2, 2, 2, 1, 0, -1];
int[] y1 = [-2, -1, 0, 1, 2, 2, 2, 2];
return tap == 0 ? (y0[direction] * stride) + x0[direction] : (y1[direction] * stride) + x1[direction];
}
}
Loading…
Cancel
Save