From a0dfc25d4c018e1c091ac72ad9050dc43320d8bd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 27 Aug 2026 01:38:04 +1000 Subject: [PATCH] Implement SIMD-first AV1 deblocking --- HEIF_IMPLEMENTATION_PLAN.md | 7 +- .../LoopFilter/Av1DeblockingFilter.cs | 1079 +++++++++++++++++ .../LoopFilter/Av1LoopFilterDecoder.cs | 45 +- .../LoopFilter/Av1LoopFilterKernels.cs | 427 ------- .../Heif/Av1DeblockingFilterBenchmarks.cs | 145 +++ .../Heif/Av1/Av1DeblockingFilterTests.cs | 476 ++++++++ 6 files changed, 1738 insertions(+), 441 deletions(-) create mode 100644 src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.cs delete mode 100644 src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterKernels.cs create mode 100644 tests/ImageSharp.Benchmarks/Codecs/Heif/Av1DeblockingFilterBenchmarks.cs create mode 100644 tests/ImageSharp.Tests/Formats/Heif/Av1/Av1DeblockingFilterTests.cs diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 14a4dd183..66b6bef81 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/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` 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. diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.cs new file mode 100644 index 000000000..eb4748e07 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.cs @@ -0,0 +1,1079 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; + +/// +/// Applies the AV1 deblocking kernels to four-sample edge segments. +/// +internal static class Av1DeblockingFilter +{ + /// + /// Defines orientation- and storage-specific access to the four samples running along one edge segment. + /// + /// The reconstructed sample storage type. + private interface IEdgeOperator + where TSample : unmanaged + { + /// + /// Loads four samples at one signed distance across the edge. + /// + /// The first element in the plane storage. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signed sample distance from Q0. + /// The widened samples ordered along the edge. + public static abstract Vector128 LoadVector(ref TSample samples, int q0Offset, int stride, int distance); + + /// + /// Stores four samples at one signed distance across the edge. + /// + /// The first element in the plane storage. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signed sample distance from Q0. + /// The widened samples ordered along the edge. + public static abstract void StoreVector(ref TSample samples, int q0Offset, int stride, int distance, Vector128 value); + + /// + /// Loads one sample at a signed distance across and an offset along the edge. + /// + /// The first element in the plane storage. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signed sample distance from Q0. + /// The sample offset along the edge. + /// The selected sample. + public static abstract int LoadScalar(ref TSample samples, int q0Offset, int stride, int distance, int index); + + /// + /// Stores one sample at a signed distance across and an offset along the edge. + /// + /// The first element in the plane storage. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signed sample distance from Q0. + /// The sample offset along the edge. + /// The filtered sample. + public static abstract void StoreScalar(ref TSample samples, int q0Offset, int stride, int distance, int index, int value); + } + + /// + /// Filters four rows crossing one vertical boundary in eight-bit storage. + /// + /// The plane storage containing the edge and its neighboring samples. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signaled 4-, 6-, 8-, or 14-tap filter length. + /// The threshold for adjacent samples on either side of the edge. + /// The threshold for the discontinuity across the edge. + /// The threshold that selects the narrow high-variance adjustment. + public static void FilterVertical( + Span samples, + int q0Offset, + int stride, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold) + => Filter(samples, q0Offset, stride, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, 8); + + /// + /// Filters four columns crossing one horizontal boundary in eight-bit storage. + /// + /// The plane storage containing the edge and its neighboring samples. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signaled 4-, 6-, 8-, or 14-tap filter length. + /// The threshold for adjacent samples on either side of the edge. + /// The threshold for the discontinuity across the edge. + /// The threshold that selects the narrow high-variance adjustment. + public static void FilterHorizontal( + Span samples, + int q0Offset, + int stride, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold) + => Filter(samples, q0Offset, stride, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, 8); + + /// + /// Filters four rows crossing one vertical boundary in 16-bit storage. + /// + /// The plane storage containing the edge and its neighboring samples. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signaled 4-, 6-, 8-, or 14-tap filter length. + /// The eight-bit-domain threshold for adjacent samples on either side of the edge. + /// The eight-bit-domain threshold for the discontinuity across the edge. + /// The eight-bit-domain threshold that selects the narrow high-variance adjustment. + /// The sample bit depth. + public static void FilterVertical( + Span samples, + int q0Offset, + int stride, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold, + int bitDepth) + => Filter(samples, q0Offset, stride, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, bitDepth); + + /// + /// Filters four columns crossing one horizontal boundary in 16-bit storage. + /// + /// The plane storage containing the edge and its neighboring samples. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signaled 4-, 6-, 8-, or 14-tap filter length. + /// The eight-bit-domain threshold for adjacent samples on either side of the edge. + /// The eight-bit-domain threshold for the discontinuity across the edge. + /// The eight-bit-domain threshold that selects the narrow high-variance adjustment. + /// The sample bit depth. + public static void FilterHorizontal( + Span samples, + int q0Offset, + int stride, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold, + int bitDepth) + => Filter(samples, q0Offset, stride, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, bitDepth); + + /// + /// Selects the packed or scalar kernel through one closed edge-access operator. + /// + /// The reconstructed sample storage type. + /// The orientation- and storage-specific edge operator. + /// The plane storage containing the edge and its neighboring samples. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signaled filter length. + /// The eight-bit-domain adjacent-sample threshold. + /// The eight-bit-domain edge-discontinuity threshold. + /// The eight-bit-domain high-edge-variance threshold. + /// The sample bit depth. + private static void Filter( + Span samples, + int q0Offset, + int stride, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold, + int bitDepth) + where TSample : unmanaged + where TEdgeOperator : struct, IEdgeOperator + { + ref TSample sampleBase = ref MemoryMarshal.GetReference(samples); + if (Vector128.IsHardwareAccelerated) + { + FilterVector(ref sampleBase, q0Offset, stride, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, bitDepth); + return; + } + + FilterScalar(ref sampleBase, q0Offset, stride, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, bitDepth); + } + + /// + /// Applies one packed AV1 kernel with each 32-bit lane representing one row or column along the edge. + /// + /// The reconstructed sample storage type. + /// The orientation- and storage-specific edge operator. + /// The first element in the plane storage. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signaled filter length. + /// The eight-bit-domain adjacent-sample threshold. + /// The eight-bit-domain edge-discontinuity threshold. + /// The eight-bit-domain high-edge-variance threshold. + /// The sample bit depth. + private static void FilterVector( + ref TSample samples, + int q0Offset, + int stride, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold, + int bitDepth) + where TSample : unmanaged + where TEdgeOperator : struct, IEdgeOperator + { + int radius = GetFilterRadius(filterLength); + InlineArray14> window = default; + + // AV1 names the samples p6..p0,q0..q6. Loading them into that exact order lets the packed equations below + // follow the normative scalar formulas without lane shuffles or an intermediate per-edge sample buffer. + for (int distance = 1; distance <= radius; distance++) + { + window[7 - distance] = TEdgeOperator.LoadVector(ref samples, q0Offset, stride, -distance); + window[6 + distance] = TEdgeOperator.LoadVector(ref samples, q0Offset, stride, distance - 1); + } + + FilterSamples(ref window, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, bitDepth); + + int modifiedRadius = GetModifiedRadius(filterLength); + for (int distance = 1; distance <= modifiedRadius; distance++) + { + TEdgeOperator.StoreVector(ref samples, q0Offset, stride, -distance, window[7 - distance]); + TEdgeOperator.StoreVector(ref samples, q0Offset, stride, distance - 1, window[6 + distance]); + } + } + + /// + /// Applies the scalar fallback to each of the four samples along an edge. + /// + /// The reconstructed sample storage type. + /// The orientation- and storage-specific edge operator. + /// The first element in the plane storage. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signaled filter length. + /// The eight-bit-domain adjacent-sample threshold. + /// The eight-bit-domain edge-discontinuity threshold. + /// The eight-bit-domain high-edge-variance threshold. + /// The sample bit depth. + private static void FilterScalar( + ref TSample samples, + int q0Offset, + int stride, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold, + int bitDepth) + where TSample : unmanaged + where TEdgeOperator : struct, IEdgeOperator + { + int radius = GetFilterRadius(filterLength); + int modifiedRadius = GetModifiedRadius(filterLength); + InlineArray14 window = default; + Span sampleWindow = window; + + for (int index = 0; index < 4; index++) + { + // The fixed p6..q6 window is reused for all four lanes. Every value consumed by the selected kernel is + // overwritten before filtering, so the fallback requires neither per-lane clearing nor stack allocation. + for (int distance = 1; distance <= radius; distance++) + { + sampleWindow[7 - distance] = TEdgeOperator.LoadScalar(ref samples, q0Offset, stride, -distance, index); + sampleWindow[6 + distance] = TEdgeOperator.LoadScalar(ref samples, q0Offset, stride, distance - 1, index); + } + + FilterSamples(sampleWindow, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, bitDepth); + + for (int distance = 1; distance <= modifiedRadius; distance++) + { + TEdgeOperator.StoreScalar(ref samples, q0Offset, stride, -distance, index, sampleWindow[7 - distance]); + TEdgeOperator.StoreScalar(ref samples, q0Offset, stride, distance - 1, index, sampleWindow[6 + distance]); + } + } + } + + /// + /// Gets the number of samples read from each side of an edge for a filter length. + /// + /// The AV1 filter length. + /// The sample radius on either side of the edge. + private static int GetFilterRadius(int filterLength) => filterLength switch + { + 4 => 2, + 6 => 3, + 8 => 4, + 14 => 7, + _ => 0 + }; + + /// + /// Gets the number of samples that a filter can modify on each side of an edge. + /// + /// The AV1 filter length. + /// The modified sample radius on either side of the edge. + private static int GetModifiedRadius(int filterLength) => filterLength switch + { + 4 or 6 => 2, + 8 => 3, + 14 => 6, + _ => 0 + }; + + /// + /// Selects and applies the packed filter arithmetic for four edge lanes. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The signaled filter length. + /// The eight-bit-domain adjacent-sample threshold. + /// The eight-bit-domain edge-discontinuity threshold. + /// The eight-bit-domain high-edge-variance threshold. + /// The sample bit depth. + private static void FilterSamples( + ref InlineArray14> samples, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold, + int bitDepth) + { + int thresholdScale = 1 << (bitDepth - 8); + Vector128 limitVector = Vector128.Create(limit * thresholdScale); + Vector128 boundaryLimitVector = Vector128.Create(boundaryLimit * thresholdScale); + Vector128 varianceThresholdVector = Vector128.Create(highEdgeVarianceThreshold * thresholdScale); + Vector128 flatnessThresholdVector = Vector128.Create(thresholdScale); + + switch (filterLength) + { + case 4: + Filter4(ref samples, IsFilter2Enabled(ref samples, limitVector, boundaryLimitVector), varianceThresholdVector, bitDepth); + break; + case 6: + Filter6( + ref samples, + IsChromaFilterEnabled(ref samples, limitVector, boundaryLimitVector), + IsChromaFlat(ref samples, flatnessThresholdVector), + varianceThresholdVector, + bitDepth); + break; + case 8: + Filter8( + ref samples, + IsFilterEnabled(ref samples, limitVector, boundaryLimitVector), + IsFlat(ref samples, flatnessThresholdVector), + varianceThresholdVector, + bitDepth); + break; + case 14: + Filter14( + ref samples, + IsFilterEnabled(ref samples, limitVector, boundaryLimitVector), + IsFlat(ref samples, flatnessThresholdVector), + IsOuterFlat(ref samples, flatnessThresholdVector), + varianceThresholdVector, + bitDepth); + break; + } + } + + /// + /// Selects and applies the scalar filter arithmetic for one edge lane. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// The signaled filter length. + /// The eight-bit-domain adjacent-sample threshold. + /// The eight-bit-domain edge-discontinuity threshold. + /// The eight-bit-domain high-edge-variance threshold. + /// The sample bit depth. + private static void FilterSamples( + Span samples, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold, + int bitDepth) + { + int thresholdScale = 1 << (bitDepth - 8); + int scaledLimit = limit * thresholdScale; + int scaledBoundaryLimit = boundaryLimit * thresholdScale; + + switch (filterLength) + { + case 4: + Filter4(samples, IsFilter2Enabled(samples, scaledLimit, scaledBoundaryLimit), highEdgeVarianceThreshold * thresholdScale, bitDepth); + break; + case 6: + Filter6( + samples, + IsChromaFilterEnabled(samples, scaledLimit, scaledBoundaryLimit), + IsChromaFlat(samples, thresholdScale), + highEdgeVarianceThreshold * thresholdScale, + bitDepth); + break; + case 8: + Filter8( + samples, + IsFilterEnabled(samples, scaledLimit, scaledBoundaryLimit), + IsFlat(samples, thresholdScale), + highEdgeVarianceThreshold * thresholdScale, + bitDepth); + break; + case 14: + Filter14( + samples, + IsFilterEnabled(samples, scaledLimit, scaledBoundaryLimit), + IsFlat(samples, thresholdScale), + IsOuterFlat(samples, thresholdScale), + highEdgeVarianceThreshold * thresholdScale, + bitDepth); + break; + } + } + + /// + /// Determines which packed lanes satisfy the four-tap AV1 filter mask. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The scaled adjacent-sample threshold. + /// The scaled edge-discontinuity threshold. + /// A mask containing all bits set in each enabled lane. + private static Vector128 IsFilter2Enabled(ref InlineArray14> samples, Vector128 limit, Vector128 boundaryLimit) + => Vector128.LessThanOrEqual(Vector128.Abs(samples[5] - samples[6]), limit) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[8] - samples[7]), limit) + & Vector128.LessThanOrEqual((Vector128.Abs(samples[6] - samples[7]) * 2) + (Vector128.Abs(samples[5] - samples[8]) >> 1), boundaryLimit); + + /// + /// Determines which packed lanes satisfy the eight- and fourteen-tap AV1 filter mask. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The scaled adjacent-sample threshold. + /// The scaled edge-discontinuity threshold. + /// A mask containing all bits set in each enabled lane. + private static Vector128 IsFilterEnabled(ref InlineArray14> samples, Vector128 limit, Vector128 boundaryLimit) + => Vector128.LessThanOrEqual(Vector128.Abs(samples[3] - samples[4]), limit) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[4] - samples[5]), limit) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[5] - samples[6]), limit) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[8] - samples[7]), limit) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[9] - samples[8]), limit) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[10] - samples[9]), limit) + & Vector128.LessThanOrEqual((Vector128.Abs(samples[6] - samples[7]) * 2) + (Vector128.Abs(samples[5] - samples[8]) >> 1), boundaryLimit); + + /// + /// Determines which packed lanes satisfy the six-tap chroma filter mask. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The scaled adjacent-sample threshold. + /// The scaled edge-discontinuity threshold. + /// A mask containing all bits set in each enabled lane. + private static Vector128 IsChromaFilterEnabled(ref InlineArray14> samples, Vector128 limit, Vector128 boundaryLimit) + => Vector128.LessThanOrEqual(Vector128.Abs(samples[4] - samples[5]), limit) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[5] - samples[6]), limit) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[8] - samples[7]), limit) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[9] - samples[8]), limit) + & Vector128.LessThanOrEqual((Vector128.Abs(samples[6] - samples[7]) * 2) + (Vector128.Abs(samples[5] - samples[8]) >> 1), boundaryLimit); + + /// + /// Determines which packed lanes satisfy the inner flatness mask. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The scaled flatness threshold. + /// A mask containing all bits set in each flat lane. + private static Vector128 IsFlat(ref InlineArray14> samples, Vector128 threshold) + => Vector128.LessThanOrEqual(Vector128.Abs(samples[5] - samples[6]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[8] - samples[7]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[4] - samples[6]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[9] - samples[7]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[3] - samples[6]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[10] - samples[7]), threshold); + + /// + /// Determines which packed lanes satisfy the chroma flatness mask. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The scaled flatness threshold. + /// A mask containing all bits set in each flat lane. + private static Vector128 IsChromaFlat(ref InlineArray14> samples, Vector128 threshold) + => Vector128.LessThanOrEqual(Vector128.Abs(samples[5] - samples[6]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[8] - samples[7]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[4] - samples[6]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[9] - samples[7]), threshold); + + /// + /// Determines which packed lanes satisfy the outer fourteen-tap flatness mask. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The scaled flatness threshold. + /// A mask containing all bits set in each flat lane. + private static Vector128 IsOuterFlat(ref InlineArray14> samples, Vector128 threshold) + => Vector128.LessThanOrEqual(Vector128.Abs(samples[1] - samples[6]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[11] - samples[7]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[2] - samples[6]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[12] - samples[7]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[0] - samples[6]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[13] - samples[7]), threshold); + + /// + /// Applies the packed narrow signed-saturating AV1 edge adjustment. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The per-lane filter-enable mask. + /// The scaled high-edge-variance threshold. + /// The sample bit depth. + private static void Filter4( + ref InlineArray14> samples, + Vector128 filterEnabled, + Vector128 highEdgeVarianceThreshold, + int bitDepth) + { + Vector128 p1 = samples[5]; + Vector128 p0 = samples[6]; + Vector128 q0 = samples[7]; + Vector128 q1 = samples[8]; + Vector128 highEdgeVariance = Vector128.GreaterThan(Vector128.Abs(p1 - p0), highEdgeVarianceThreshold) + | Vector128.GreaterThan(Vector128.Abs(q1 - q0), highEdgeVarianceThreshold); + + int signedOffset = 128 << (bitDepth - 8); + Vector128 minimum = Vector128.Create(-signedOffset); + Vector128 maximum = Vector128.Create(signedOffset - 1); + Vector128 offset = Vector128.Create(signedOffset); + Vector128 signedP1 = p1 - offset; + Vector128 signedP0 = p0 - offset; + Vector128 signedQ0 = q0 - offset; + Vector128 signedQ1 = q1 - offset; + + // libaom performs every delta operation in the signed sample domain. Saturating only the final samples is + // not equivalent because the intermediate delta can clip before the asymmetric +4/+3 rounding is applied. + Vector128 filter = Vector128.ConditionalSelect(highEdgeVariance, Vector128.Clamp(signedP1 - signedQ1, minimum, maximum), Vector128.Zero); + filter = Vector128.Clamp(filter + (3 * (signedQ0 - signedP0)), minimum, maximum) & filterEnabled; + + Vector128 filter1 = Vector128.Clamp(filter + Vector128.Create(4), minimum, maximum) >> 3; + Vector128 filter2 = Vector128.Clamp(filter + Vector128.Create(3), minimum, maximum) >> 3; + samples[7] = Vector128.Clamp(signedQ0 - filter1, minimum, maximum) + offset; + samples[6] = Vector128.Clamp(signedP0 + filter2, minimum, maximum) + offset; + + Vector128 outerFilter = ((filter1 + Vector128.One) >> 1) & ~highEdgeVariance; + samples[8] = Vector128.Clamp(signedQ1 - outerFilter, minimum, maximum) + offset; + samples[5] = Vector128.Clamp(signedP1 + outerFilter, minimum, maximum) + offset; + } + + /// + /// Applies the packed six-tap chroma filter or its four-tap fallback. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The per-lane filter-enable mask. + /// The per-lane inner-flatness mask. + /// The scaled high-edge-variance threshold. + /// The sample bit depth. + private static void Filter6( + ref InlineArray14> samples, + Vector128 filterEnabled, + Vector128 flat, + Vector128 highEdgeVarianceThreshold, + int bitDepth) + { + Vector128 p2 = samples[4]; + Vector128 p1 = samples[5]; + Vector128 p0 = samples[6]; + Vector128 q0 = samples[7]; + Vector128 q1 = samples[8]; + Vector128 q2 = samples[9]; + Vector128 wideFilter = filterEnabled & flat; + + Filter4(ref samples, filterEnabled, highEdgeVarianceThreshold, bitDepth); + if (Vector128.EqualsAll(wideFilter, Vector128.Zero)) + { + return; + } + + Vector128 filteredP1 = RoundPowerOfTwo((3 * p2) + (2 * p1) + (2 * p0) + q0, 3); + Vector128 filteredP0 = RoundPowerOfTwo(p2 + (2 * p1) + (2 * p0) + (2 * q0) + q1, 3); + Vector128 filteredQ0 = RoundPowerOfTwo(p1 + (2 * p0) + (2 * q0) + (2 * q1) + q2, 3); + Vector128 filteredQ1 = RoundPowerOfTwo(p0 + (2 * q0) + (2 * q1) + (3 * q2), 3); + samples[5] = Vector128.ConditionalSelect(wideFilter, filteredP1, samples[5]); + samples[6] = Vector128.ConditionalSelect(wideFilter, filteredP0, samples[6]); + samples[7] = Vector128.ConditionalSelect(wideFilter, filteredQ0, samples[7]); + samples[8] = Vector128.ConditionalSelect(wideFilter, filteredQ1, samples[8]); + } + + /// + /// Applies the packed eight-tap luma filter or its four-tap fallback. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The per-lane filter-enable mask. + /// The per-lane inner-flatness mask. + /// The scaled high-edge-variance threshold. + /// The sample bit depth. + private static void Filter8( + ref InlineArray14> samples, + Vector128 filterEnabled, + Vector128 flat, + Vector128 highEdgeVarianceThreshold, + int bitDepth) + { + Vector128 p3 = samples[3]; + Vector128 p2 = samples[4]; + Vector128 p1 = samples[5]; + Vector128 p0 = samples[6]; + Vector128 q0 = samples[7]; + Vector128 q1 = samples[8]; + Vector128 q2 = samples[9]; + Vector128 q3 = samples[10]; + Vector128 wideFilter = filterEnabled & flat; + + Filter4(ref samples, filterEnabled, highEdgeVarianceThreshold, bitDepth); + if (Vector128.EqualsAll(wideFilter, Vector128.Zero)) + { + return; + } + + Vector128 filteredP2 = RoundPowerOfTwo((3 * p3) + (2 * p2) + p1 + p0 + q0, 3); + Vector128 filteredP1 = RoundPowerOfTwo((2 * p3) + p2 + (2 * p1) + p0 + q0 + q1, 3); + Vector128 filteredP0 = RoundPowerOfTwo(p3 + p2 + p1 + (2 * p0) + q0 + q1 + q2, 3); + Vector128 filteredQ0 = RoundPowerOfTwo(p2 + p1 + p0 + (2 * q0) + q1 + q2 + q3, 3); + Vector128 filteredQ1 = RoundPowerOfTwo(p1 + p0 + q0 + (2 * q1) + q2 + (2 * q3), 3); + Vector128 filteredQ2 = RoundPowerOfTwo(p0 + q0 + q1 + (2 * q2) + (3 * q3), 3); + samples[4] = Vector128.ConditionalSelect(wideFilter, filteredP2, samples[4]); + samples[5] = Vector128.ConditionalSelect(wideFilter, filteredP1, samples[5]); + samples[6] = Vector128.ConditionalSelect(wideFilter, filteredP0, samples[6]); + samples[7] = Vector128.ConditionalSelect(wideFilter, filteredQ0, samples[7]); + samples[8] = Vector128.ConditionalSelect(wideFilter, filteredQ1, samples[8]); + samples[9] = Vector128.ConditionalSelect(wideFilter, filteredQ2, samples[9]); + } + + /// + /// Applies the packed fourteen-tap luma filter or its eight- and four-tap fallbacks. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The per-lane filter-enable mask. + /// The per-lane inner-flatness mask. + /// The per-lane outer-flatness mask. + /// The scaled high-edge-variance threshold. + /// The sample bit depth. + private static void Filter14( + ref InlineArray14> samples, + Vector128 filterEnabled, + Vector128 flat, + Vector128 outerFlat, + Vector128 highEdgeVarianceThreshold, + int bitDepth) + { + Vector128 p6 = samples[0]; + Vector128 p5 = samples[1]; + Vector128 p4 = samples[2]; + Vector128 p3 = samples[3]; + Vector128 p2 = samples[4]; + Vector128 p1 = samples[5]; + Vector128 p0 = samples[6]; + Vector128 q0 = samples[7]; + Vector128 q1 = samples[8]; + Vector128 q2 = samples[9]; + Vector128 q3 = samples[10]; + Vector128 q4 = samples[11]; + Vector128 q5 = samples[12]; + Vector128 q6 = samples[13]; + Vector128 wideFilter = filterEnabled & flat & outerFlat; + + // The eight-tap routine first produces the normative fallback. Lanes satisfying the outer flatness mask are + // then replaced with the wider results, matching libaom's mask blend without evaluating lanes independently. + Filter8(ref samples, filterEnabled, flat, highEdgeVarianceThreshold, bitDepth); + if (Vector128.EqualsAll(wideFilter, Vector128.Zero)) + { + return; + } + + Vector128 filteredP5 = RoundPowerOfTwo((7 * p6) + (2 * p5) + (2 * p4) + p3 + p2 + p1 + p0 + q0, 4); + Vector128 filteredP4 = RoundPowerOfTwo((5 * p6) + (2 * p5) + (2 * p4) + (2 * p3) + p2 + p1 + p0 + q0 + q1, 4); + Vector128 filteredP3 = RoundPowerOfTwo((4 * p6) + p5 + (2 * p4) + (2 * p3) + (2 * p2) + p1 + p0 + q0 + q1 + q2, 4); + Vector128 filteredP2 = RoundPowerOfTwo((3 * p6) + p5 + p4 + (2 * p3) + (2 * p2) + (2 * p1) + p0 + q0 + q1 + q2 + q3, 4); + Vector128 filteredP1 = RoundPowerOfTwo((2 * p6) + p5 + p4 + p3 + (2 * p2) + (2 * p1) + (2 * p0) + q0 + q1 + q2 + q3 + q4, 4); + Vector128 filteredP0 = RoundPowerOfTwo(p6 + p5 + p4 + p3 + p2 + (2 * p1) + (2 * p0) + (2 * q0) + q1 + q2 + q3 + q4 + q5, 4); + Vector128 filteredQ0 = RoundPowerOfTwo(p5 + p4 + p3 + p2 + p1 + (2 * p0) + (2 * q0) + (2 * q1) + q2 + q3 + q4 + q5 + q6, 4); + Vector128 filteredQ1 = RoundPowerOfTwo(p4 + p3 + p2 + p1 + p0 + (2 * q0) + (2 * q1) + (2 * q2) + q3 + q4 + q5 + (2 * q6), 4); + Vector128 filteredQ2 = RoundPowerOfTwo(p3 + p2 + p1 + p0 + q0 + (2 * q1) + (2 * q2) + (2 * q3) + q4 + q5 + (3 * q6), 4); + Vector128 filteredQ3 = RoundPowerOfTwo(p2 + p1 + p0 + q0 + q1 + (2 * q2) + (2 * q3) + (2 * q4) + q5 + (4 * q6), 4); + Vector128 filteredQ4 = RoundPowerOfTwo(p1 + p0 + q0 + q1 + q2 + (2 * q3) + (2 * q4) + (2 * q5) + (5 * q6), 4); + Vector128 filteredQ5 = RoundPowerOfTwo(p0 + q0 + q1 + q2 + q3 + (2 * q4) + (2 * q5) + (7 * q6), 4); + + samples[1] = Vector128.ConditionalSelect(wideFilter, filteredP5, samples[1]); + samples[2] = Vector128.ConditionalSelect(wideFilter, filteredP4, samples[2]); + samples[3] = Vector128.ConditionalSelect(wideFilter, filteredP3, samples[3]); + samples[4] = Vector128.ConditionalSelect(wideFilter, filteredP2, samples[4]); + samples[5] = Vector128.ConditionalSelect(wideFilter, filteredP1, samples[5]); + samples[6] = Vector128.ConditionalSelect(wideFilter, filteredP0, samples[6]); + samples[7] = Vector128.ConditionalSelect(wideFilter, filteredQ0, samples[7]); + samples[8] = Vector128.ConditionalSelect(wideFilter, filteredQ1, samples[8]); + samples[9] = Vector128.ConditionalSelect(wideFilter, filteredQ2, samples[9]); + samples[10] = Vector128.ConditionalSelect(wideFilter, filteredQ3, samples[10]); + samples[11] = Vector128.ConditionalSelect(wideFilter, filteredQ4, samples[11]); + samples[12] = Vector128.ConditionalSelect(wideFilter, filteredQ5, samples[12]); + } + + /// + /// Determines whether a scalar lane satisfies the four-tap AV1 filter mask. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// The scaled adjacent-sample threshold. + /// The scaled edge-discontinuity threshold. + /// when the edge satisfies the filter mask. + private static bool IsFilter2Enabled(ReadOnlySpan 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; + + /// + /// Determines whether a scalar lane satisfies the eight- and fourteen-tap AV1 filter mask. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// The scaled adjacent-sample threshold. + /// The scaled edge-discontinuity threshold. + /// when the edge satisfies the filter mask. + private static bool IsFilterEnabled(ReadOnlySpan 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; + + /// + /// Determines whether a scalar lane satisfies the six-tap chroma filter mask. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// The scaled adjacent-sample threshold. + /// The scaled edge-discontinuity threshold. + /// when the edge satisfies the filter mask. + private static bool IsChromaFilterEnabled(ReadOnlySpan 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; + + /// + /// Determines whether a scalar lane satisfies the inner flatness mask. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// The scaled flatness threshold. + /// when the edge satisfies the flatness mask. + private static bool IsFlat(ReadOnlySpan 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; + + /// + /// Determines whether a scalar lane satisfies the chroma flatness mask. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// The scaled flatness threshold. + /// when the edge satisfies the flatness mask. + private static bool IsChromaFlat(ReadOnlySpan 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; + + /// + /// Determines whether a scalar lane satisfies the outer fourteen-tap flatness mask. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// The scaled flatness threshold. + /// when the edge satisfies the flatness mask. + private static bool IsOuterFlat(ReadOnlySpan 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; + + /// + /// Applies the scalar narrow signed-saturating AV1 edge adjustment. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// Whether the edge satisfies the filter mask. + /// The scaled high-edge-variance threshold. + /// The sample bit depth. + private static void Filter4(Span 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; + + 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; + } + + /// + /// Applies the scalar six-tap chroma filter or its four-tap fallback. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// Whether the edge satisfies the filter mask. + /// Whether the edge satisfies the inner-flatness mask. + /// The scaled high-edge-variance threshold. + /// The sample bit depth. + private static void Filter6(Span 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); + } + + /// + /// Applies the scalar eight-tap luma filter or its four-tap fallback. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// Whether the edge satisfies the filter mask. + /// Whether the edge satisfies the inner-flatness mask. + /// The scaled high-edge-variance threshold. + /// The sample bit depth. + private static void Filter8(Span 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); + } + + /// + /// Applies the scalar fourteen-tap luma filter or its eight- and four-tap fallbacks. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// Whether the edge satisfies the filter mask. + /// Whether the edge satisfies the inner-flatness mask. + /// Whether the edge satisfies the outer-flatness mask. + /// The scaled high-edge-variance threshold. + /// The sample bit depth. + private static void Filter14(Span 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 endpoints extend the normative 13-tap window without reading beyond p6 and 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); + } + + /// + /// Rounds a packed integer while dividing by a power of two. + /// + /// The packed integers to round. + /// The base-two divisor exponent. + /// The rounded packed quotients. + private static Vector128 RoundPowerOfTwo(Vector128 value, int bitCount) + => (value + Vector128.Create(1 << (bitCount - 1))) >> bitCount; + + /// + /// Rounds a scalar integer while dividing by a power of two. + /// + /// The integer to round. + /// The base-two divisor exponent. + /// The rounded quotient. + private static int RoundPowerOfTwo(int value, int bitCount) + => (value + (1 << (bitCount - 1))) >> bitCount; + + /// + /// Accesses four rows across a vertical edge in eight-bit storage. + /// + private readonly struct VerticalByteEdgeOperator : IEdgeOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 LoadVector(ref byte samples, int q0Offset, int stride, int distance) + => Vector128.Create( + (int)Unsafe.Add(ref samples, q0Offset + distance), + Unsafe.Add(ref samples, q0Offset + stride + distance), + Unsafe.Add(ref samples, q0Offset + (2 * stride) + distance), + Unsafe.Add(ref samples, q0Offset + (3 * stride) + distance)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StoreVector(ref byte samples, int q0Offset, int stride, int distance, Vector128 value) + { + Unsafe.Add(ref samples, q0Offset + distance) = (byte)value.GetElement(0); + Unsafe.Add(ref samples, q0Offset + stride + distance) = (byte)value.GetElement(1); + Unsafe.Add(ref samples, q0Offset + (2 * stride) + distance) = (byte)value.GetElement(2); + Unsafe.Add(ref samples, q0Offset + (3 * stride) + distance) = (byte)value.GetElement(3); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int LoadScalar(ref byte samples, int q0Offset, int stride, int distance, int index) + => Unsafe.Add(ref samples, q0Offset + (index * stride) + distance); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StoreScalar(ref byte samples, int q0Offset, int stride, int distance, int index, int value) + => Unsafe.Add(ref samples, q0Offset + (index * stride) + distance) = (byte)value; + } + + /// + /// Accesses four columns across a horizontal edge in eight-bit storage. + /// + private readonly struct HorizontalByteEdgeOperator : IEdgeOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 LoadVector(ref byte samples, int q0Offset, int stride, int distance) + { + ref byte source = ref Unsafe.Add(ref samples, q0Offset + (distance * stride)); + uint packed = Unsafe.ReadUnaligned(ref source); + Vector128 widened = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte()); + return Vector128.WidenLower(widened).AsInt32(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StoreVector(ref byte samples, int q0Offset, int stride, int distance, Vector128 value) + { + Vector128 narrowed16 = Vector128.Narrow(value.AsUInt32(), Vector128.Zero); + Vector128 narrowed8 = Vector128.Narrow(narrowed16, Vector128.Zero); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref samples, q0Offset + (distance * stride)), narrowed8.AsUInt32().ToScalar()); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int LoadScalar(ref byte samples, int q0Offset, int stride, int distance, int index) + => Unsafe.Add(ref samples, q0Offset + (distance * stride) + index); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StoreScalar(ref byte samples, int q0Offset, int stride, int distance, int index, int value) + => Unsafe.Add(ref samples, q0Offset + (distance * stride) + index) = (byte)value; + } + + /// + /// Accesses four rows across a vertical edge in 16-bit storage. + /// + private readonly struct VerticalUInt16EdgeOperator : IEdgeOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 LoadVector(ref ushort samples, int q0Offset, int stride, int distance) + => Vector128.Create( + (int)Unsafe.Add(ref samples, q0Offset + distance), + Unsafe.Add(ref samples, q0Offset + stride + distance), + Unsafe.Add(ref samples, q0Offset + (2 * stride) + distance), + Unsafe.Add(ref samples, q0Offset + (3 * stride) + distance)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StoreVector(ref ushort samples, int q0Offset, int stride, int distance, Vector128 value) + { + Unsafe.Add(ref samples, q0Offset + distance) = (ushort)value.GetElement(0); + Unsafe.Add(ref samples, q0Offset + stride + distance) = (ushort)value.GetElement(1); + Unsafe.Add(ref samples, q0Offset + (2 * stride) + distance) = (ushort)value.GetElement(2); + Unsafe.Add(ref samples, q0Offset + (3 * stride) + distance) = (ushort)value.GetElement(3); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int LoadScalar(ref ushort samples, int q0Offset, int stride, int distance, int index) + => Unsafe.Add(ref samples, q0Offset + (index * stride) + distance); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StoreScalar(ref ushort samples, int q0Offset, int stride, int distance, int index, int value) + => Unsafe.Add(ref samples, q0Offset + (index * stride) + distance) = (ushort)value; + } + + /// + /// Accesses four columns across a horizontal edge in 16-bit storage. + /// + private readonly struct HorizontalUInt16EdgeOperator : IEdgeOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 LoadVector(ref ushort samples, int q0Offset, int stride, int distance) + { + ref ushort source = ref Unsafe.Add(ref samples, q0Offset + (distance * stride)); + ulong packed = Unsafe.ReadUnaligned(ref Unsafe.As(ref source)); + return Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsUInt16()).AsInt32(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StoreVector(ref ushort samples, int q0Offset, int stride, int distance, Vector128 value) + { + Vector64 narrowed = Vector128.Narrow(value, Vector128.Zero).AsUInt16().GetLower(); + ref byte destination = ref Unsafe.As(ref Unsafe.Add(ref samples, q0Offset + (distance * stride))); + Unsafe.WriteUnaligned(ref destination, narrowed.AsUInt64().ToScalar()); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int LoadScalar(ref ushort samples, int q0Offset, int stride, int distance, int index) + => Unsafe.Add(ref samples, q0Offset + (distance * stride) + index); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StoreScalar(ref ushort samples, int q0Offset, int stride, int distance, int index, int value) + => Unsafe.Add(ref samples, q0Offset + (distance * stride) + index) = (ushort)value; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterDecoder.cs index fdc9f7ae7..7a4e2ef94 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterDecoder.cs +++ b/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, diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterKernels.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterKernels.cs deleted file mode 100644 index 79ab8b4a8..000000000 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterKernels.cs +++ /dev/null @@ -1,427 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; - -/// -/// Applies the scalar AV1 deblocking kernels to low- and high-bit-depth sample edges. -/// -internal static class Av1LoopFilterKernels -{ - /// - /// The number of samples processed along one transform edge at a time. - /// - private const int EdgeLaneCount = 4; - - /// - /// The number of samples read by the widest AV1 deblocking kernel. - /// - private const int MaximumKernelSampleCount = 14; - - /// - /// Applies one low-bit-depth deblocking kernel to four adjacent samples along an edge. - /// - /// The plane storage containing the edge and its neighboring samples. - /// The offset of the first sample on the Q side of the edge. - /// The storage distance between adjacent samples across the edge. - /// The storage distance between adjacent samples along the edge. - /// The signaled 4-, 6-, 8-, or 14-tap filter length. - /// The threshold for adjacent samples on either side of the edge. - /// The threshold for the discontinuity across the edge. - /// The threshold that selects the narrow high-variance adjustment. - public static void FilterLowBitDepthEdge( - Span samples, - int q0Offset, - int pixelStep, - int lineStep, - int filterLength, - int limit, - int boundaryLimit, - int highEdgeVarianceThreshold) - { - int radius = GetFilterRadius(filterLength); - Span 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]; - } - } - } - - /// - /// Applies one high-bit-depth deblocking kernel to four adjacent samples along an edge. - /// - /// The plane storage containing the edge and its neighboring samples. - /// The offset of the first sample on the Q side of the edge. - /// The storage distance between adjacent samples across the edge. - /// The storage distance between adjacent samples along the edge. - /// The signaled 4-, 6-, 8-, or 14-tap filter length. - /// The eight-bit-domain threshold for adjacent samples on either side of the edge. - /// The eight-bit-domain threshold for the discontinuity across the edge. - /// The eight-bit-domain threshold that selects the narrow adjustment. - /// The sample bit depth. - public static void FilterHighBitDepthEdge( - Span samples, - int q0Offset, - int pixelStep, - int lineStep, - int filterLength, - int limit, - int boundaryLimit, - int highEdgeVarianceThreshold, - int bitDepth) - { - int radius = GetFilterRadius(filterLength); - Span 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]; - } - } - } - - /// - /// Gets the number of samples read from each side of an edge for a filter length. - /// - /// The AV1 filter length. - /// The sample radius on either side of the edge. - private static int GetFilterRadius(int filterLength) => filterLength switch - { - 4 => 2, - 6 => 3, - 8 => 4, - 14 => 7, - _ => 0 - }; - - /// - /// Selects and applies the AV1 filter arithmetic for one edge lane. - /// - /// The p6..p0,q0..q6 scratch samples. - /// The AV1 filter length. - /// The eight-bit-domain adjacent-sample threshold. - /// The eight-bit-domain edge-discontinuity threshold. - /// The eight-bit-domain high-edge-variance threshold. - /// The sample bit depth. - private static void FilterSamples( - Span 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; - } - } - - /// - /// Determines whether a 4-tap filter may modify an edge. - /// - /// The p6..p0,q0..q6 scratch samples. - /// The scaled adjacent-sample threshold. - /// The scaled edge-discontinuity threshold. - /// when the edge satisfies the AV1 filter mask. - private static bool IsFilter2Enabled(ReadOnlySpan 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; - - /// - /// Determines whether an 8- or 14-tap luma filter may modify an edge. - /// - /// The p6..p0,q0..q6 scratch samples. - /// The scaled adjacent-sample threshold. - /// The scaled edge-discontinuity threshold. - /// when the edge satisfies the AV1 filter mask. - private static bool IsFilterEnabled(ReadOnlySpan 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; - - /// - /// Determines whether a 6-tap chroma filter may modify an edge. - /// - /// The p6..p0,q0..q6 scratch samples. - /// The scaled adjacent-sample threshold. - /// The scaled edge-discontinuity threshold. - /// when the edge satisfies the AV1 chroma filter mask. - private static bool IsChromaFilterEnabled(ReadOnlySpan 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; - - /// - /// Determines whether the inner four samples on each side form a flat edge. - /// - /// The p6..p0,q0..q6 scratch samples. - /// The scaled flatness threshold. - /// when the samples satisfy the AV1 flatness mask. - private static bool IsFlat(ReadOnlySpan 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; - - /// - /// Determines whether the three chroma samples on each side form a flat edge. - /// - /// The p6..p0,q0..q6 scratch samples. - /// The scaled flatness threshold. - /// when the samples satisfy the AV1 chroma flatness mask. - private static bool IsChromaFlat(ReadOnlySpan 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; - - /// - /// Determines whether the outer samples permit the 14-tap flat filter. - /// - /// The p6..p0,q0..q6 scratch samples. - /// The scaled flatness threshold. - /// when the outer samples satisfy the AV1 flatness mask. - private static bool IsOuterFlat(ReadOnlySpan 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; - - /// - /// Applies the narrow signed-saturating AV1 edge adjustment. - /// - /// The p6..p0,q0..q6 scratch samples. - /// Whether the filter mask permits modification. - /// The scaled high-edge-variance threshold. - /// The sample bit depth. - private static void Filter4(Span 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; - } - - /// - /// Applies the AV1 6-tap chroma filter or its 4-tap fallback. - /// - /// The p6..p0,q0..q6 scratch samples. - /// Whether the filter mask permits modification. - /// Whether the samples permit the wider flat filter. - /// The scaled high-edge-variance threshold. - /// The sample bit depth. - private static void Filter6(Span 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); - } - - /// - /// Applies the AV1 8-tap luma filter or its 4-tap fallback. - /// - /// The p6..p0,q0..q6 scratch samples. - /// Whether the filter mask permits modification. - /// Whether the samples permit the wider flat filter. - /// The scaled high-edge-variance threshold. - /// The sample bit depth. - private static void Filter8(Span 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); - } - - /// - /// Applies the AV1 14-tap luma filter or its 8-/4-tap fallback. - /// - /// The p6..p0,q0..q6 scratch samples. - /// Whether the filter mask permits modification. - /// Whether the inner samples permit a wider flat filter. - /// Whether the outer samples permit the 14-tap flat filter. - /// The scaled high-edge-variance threshold. - /// The sample bit depth. - private static void Filter14(Span 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); - } - - /// - /// Rounds an integer while dividing by a power of two. - /// - /// The integer to round. - /// The base-two divisor exponent. - /// The rounded quotient. - private static int RoundPowerOfTwo(int value, int bitCount) - => (value + (1 << (bitCount - 1))) >> bitCount; -} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1DeblockingFilterBenchmarks.cs b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1DeblockingFilterBenchmarks.cs new file mode 100644 index 000000000..bb066331d --- /dev/null +++ b/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; + +/// +/// Measures AV1 deblocking across a padded full-HD reconstruction surface. +/// +[Config(typeof(Configuration))] +[MemoryDiagnoser(displayGenColumns: false)] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class Av1DeblockingFilterBenchmarks +{ + /// + /// The visible coded frame width in samples. + /// + private const int Width = 1920; + + /// + /// The coded frame height including the final padded coding-tree row for a 1080-line presentation. + /// + private const int Height = 1088; + + /// + /// The border reserved around the visible reconstruction surface. + /// + private const int Padding = 16; + + /// + /// The number of samples between adjacent padded rows. + /// + private const int Stride = Width + (2 * Padding); + + /// + /// The eight-bit padded reconstruction surface. + /// + private readonly byte[] samples8 = new byte[Stride * (Height + (2 * Padding))]; + + /// + /// The twelve-bit padded reconstruction surface. + /// + private readonly ushort[] samples12 = new ushort[Stride * (Height + (2 * Padding))]; + + /// + /// Populates smooth deterministic samples that exercise enabled narrow and wide filter masks. + /// + [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); + } + } + } + + /// + /// Measures both AV1 deblocking passes over an eight-bit full-HD frame. + /// + /// A filtered sample that keeps the output observable. + [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]; + } + + /// + /// Measures both AV1 deblocking passes over a twelve-bit full-HD frame. + /// + /// A filtered sample that keeps the output observable. + [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]; + } + + /// + /// Configures production-process measurements for hardware and scalar filtering. + /// + public sealed class Configuration : ManualConfig + { + /// + /// Initializes a new instance of the class. + /// + public Configuration() + { + this.AddJob(Job.ShortRun.WithId("Hardware").AsBaseline()); + + this.AddJob( + Job.ShortRun + .WithId("Scalar") + .WithEnvironmentVariable("DOTNET_EnableHWIntrinsic", "0")); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1DeblockingFilterTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1DeblockingFilterTests.cs new file mode 100644 index 000000000..e6027f454 --- /dev/null +++ b/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; + +/// +/// Verifies AV1 deblocking across every filter width, sample precision, orientation, and intrinsic tier. +/// +[Trait("Format", "Avif")] +public class Av1DeblockingFilterTests +{ + /// + /// The hardware configurations required to exercise packed filtering and the scalar fallback. + /// + private const HwIntrinsics Configurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic; + + /// + /// The padded plane width used to expose horizontal and vertical edge traversal. + /// + private const int Stride = 32; + + /// + /// The first Q-side coordinate, leaving the widest kernel addressable on every side. + /// + private const int EdgeCoordinate = 12; + + /// + /// Verifies exact filtering and untouched padding against an independent scalar definition. + /// + [Fact] + public void FilterMatchesIndependentDefinitionAcrossIntrinsicTiers() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateFilters, Configurations); + + /// + /// Exercises mixed flatness, high-edge-variance, disabled-mask, direction, and bit-depth cases. + /// + 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); + } + } + } + } + } + + /// + /// Verifies one eight-bit filter configuration against the independent definition. + /// + 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); + } + + /// + /// Verifies one high-bit-depth filter configuration against the independent definition. + /// + 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); + } + + /// + /// Creates four lanes that independently select the wide, shorter-wide, narrow, and high-variance results. + /// + 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) + ]; + + /// + /// Creates four lanes whose cross-edge discontinuity disables every filter width. + /// + 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()]; + } + + /// + /// Scales an eight-bit-domain sample window to the requested coded precision. + /// + private static int[] Scale(int[] values, int scale) + { + for (int index = 0; index < values.Length; index++) + { + values[index] *= scale; + } + + return values; + } + + /// + /// Places four eight-bit p6..q6 windows along one padded edge. + /// + private static void Populate(Span 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]; + } + } + } + + /// + /// Places four 16-bit p6..q6 windows along one padded edge. + /// + private static void Populate(Span 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]; + } + } + } + + /// + /// Applies the scalar AV1 definition to four eight-bit samples along one edge. + /// + private static void ApplyReference( + Span 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); + } + } + + /// + /// Applies the scalar AV1 definition to four 16-bit samples along one edge. + /// + private static void ApplyReference( + Span 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); + } + } + + /// + /// Loads one eight-bit p6..q6 window independently of the production edge operators. + /// + private static int[] LoadWindow(ReadOnlySpan 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; + } + + /// + /// Loads one 16-bit p6..q6 window independently of the production edge operators. + /// + private static int[] LoadWindow(ReadOnlySpan 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; + } + + /// + /// Stores every potentially modified sample from one eight-bit reference window. + /// + private static void StoreWindow(Span samples, bool vertical, int q0Offset, int index, int filterLength, ReadOnlySpan 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]; + } + } + + /// + /// Stores every potentially modified sample from one 16-bit reference window. + /// + private static void StoreWindow(Span samples, bool vertical, int q0Offset, int index, int filterLength, ReadOnlySpan 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]; + } + } + + /// + /// Selects and applies the normative scalar kernel for one p6..q6 window. + /// + private static void FilterReference( + Span 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); + } + + /// + /// Evaluates the AV1 filter mask for the selected reference width. + /// + private static bool IsFilterEnabled(ReadOnlySpan 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; + } + + /// + /// Evaluates the AV1 inner flatness mask for the selected reference width. + /// + private static bool IsFlat(ReadOnlySpan 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; + } + + /// + /// Evaluates the AV1 outer flatness mask for the fourteen-tap reference kernel. + /// + private static bool IsOuterFlat(ReadOnlySpan 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; + + /// + /// Applies the signed-saturating four-tap reference equations. + /// + private static void ApplyNarrowReference(Span 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; + } + + /// + /// Applies the thirteen-tap reference equations to the twelve modifiable samples. + /// + private static void ApplyWideReference(Span 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; + } +}