From 694cf797046bd97473bff0a88da26a0b856c2425 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 25 Aug 2026 01:16:34 +1000 Subject: [PATCH] Implement scalar AV1 loop filter kernels --- HEIF_IMPLEMENTATION_PLAN.md | 3 +- .../LoopFilter/Av1LoopFilterKernels.cs | 427 ++++++++++++++++++ 2 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterKernels.cs diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 831604349..59e32f71a 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -71,6 +71,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`, `Av1HighBitDepthPredictor`, `Av1ChromaFromLumaContext`, `Av1PartitionInfo`, and the scalar DC, directional, Paeth, smooth, filter-intra, and chroma-from-luma predictors | 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` | Behavioral oracle for 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. Existing managed scalar tables and predictors remain the implementation base. 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` | Port the scalar 4-, 6-, 8-, and 14-tap low/high-bit-depth filters, sharpness thresholds, still-frame intra filter-level derivation, transform-edge selection, and plane traversal before enabling the stage. Later SIMD must preserve the scalar result. This is normative AV1 image reconstruction and adds neither generic ISO BMFF models nor retained video reference state. | | `Av1FrameInfo`, `Av1TileReader`, and `Av1BlockDecoder` transform/coefficient storage | AV1 section 5.11.39 coefficient syntax and section 7.11.2 reconstruction | libaom `av1/decoder/decodetxb.c` and `av1/decoder/decoder.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve separate luma and chroma transform coefficients at monotonically advancing per-plane offsets within each superblock so reconstruction consumes the same transform-block order produced by tile parsing. | | `Av1InverseQuantizer` and `Av1InverseQuantizationLookup` | AV1 section 7.12.3 inverse quantization | libaom `aom_dsp/aom_dsp_common.h`, `av1/common/quant_common.c`, and `av1/decoder/decodetxb.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Select the per-segment matrix level, alias 64-pixel transform dimensions to their adjusted matrices, retain a flat level-15 matrix, and apply the five-bit inverse-matrix weight scale. The large managed lookup remains a single process-wide table. | | `Av1Inverse2dTransformer` and `Av1InverseTransformerFactory` | AV1 section 7.11.2 inverse transform and reconstruction | libaom `av1/common/av1_inv_txfm1d.c`, `av1/common/av1_inv_txfm2d.c`, and `av1/common/idct.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Scalar transform oracle for coefficient-row traversal, intermediate layout, stage ranges, clipping, and high-bit-depth sample addition. The managed 16-bit overload is also used as a parity oracle for the byte overload. | @@ -118,7 +119,7 @@ This assessment is based on the current source after the upstream ImageSharp mer ### AV1 decoder - The single-still `Av1Decoder` path now parses tile state before allocating and reconstructing one independently decodable frame, and it disposes the reconstruction planes after pixel conversion. It deliberately does not retain animation/video reference frames or implement `show_existing_frame` playback state. -- The reconstruction pipeline disables loop filtering, CDEF, super-resolution, loop restoration, and padding with constant flags. These are normative stages when signaled, not optional quality improvements. +- The reconstruction pipeline disables loop filtering, CDEF, super-resolution, loop restoration, and padding with constant flags. These are normative stages when signaled, not optional quality improvements. The exact scalar low/high-bit-depth AV1 4-, 6-, 8-, and 14-tap deblocking kernels are now present, but filter-level derivation, transform-edge traversal, and pipeline wiring remain required before loop filtering can be enabled. - Palette mode now reads the normative luma/chroma mode and size CDFs, neighbor color caches, high-bit-depth color syntax, diagonal color-index maps, clipped-edge padding, and direct sample prediction through the existing reconstruction pipeline. The scalar implementation matches the pinned libaom source, but an independently encoded palette AVIF fixture is still required before this path is independently verified. - Loop restoration and other normative independently decodable still-image syntax paths still contain `NotImplementedException` or equivalent unsupported branches. Tile-local palette CDF adaptation is now present; the remaining still-image frame-context behavior requires a separate source audit without introducing sequence playback state. - The frame buffer now establishes two-byte native sample storage, logical plane rows, and sample-unit block strides for 10/12-bit frames. The active intra-prediction, inverse-transform, and block-reconstruction path selects native 16-bit samples for 10/12-bit frames and has focused pipeline wiring coverage. Independently encoded high-bit-depth AVIF conformance files are still required; chroma-from-luma storage and the other normative reconstruction stages listed below remain incomplete. diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterKernels.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterKernels.cs new file mode 100644 index 000000000..79ab8b4a8 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterKernels.cs @@ -0,0 +1,427 @@ +// 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; +}