From c88afc8b2c408b5bf369aa0c89a956d0e83d66e1 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 25 Aug 2026 03:02:39 +1000 Subject: [PATCH] Implement AV1 film grain synthesis --- HEIF_IMPLEMENTATION_PLAN.md | 5 +- .../Heif/Av1/OpenBitstreamUnit/ObuReader.cs | 5 - .../Heif/Av1/Pipeline/Av1FrameDecoder.cs | 10 +- .../Pipeline/FilmGrain/Av1FilmGrainDecoder.cs | 1498 +++++++++++++++++ .../FilmGrain/Av1FilmGrainGaussianSequence.cs | 150 ++ .../Av1/Prediction/Av1PredictionDecoder.cs | 2 - 6 files changed, 1659 insertions(+), 11 deletions(-) create mode 100644 src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainDecoder.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainGaussianSequence.cs diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 2192a0f567..476c354e4a 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -78,6 +78,7 @@ This snapshot pins or classifies the available references and failures; it does | `Av1WienerFilter` | AV1 sections 7.17.4 and 7.17.5 Wiener restoration filtering and coefficient derivation | libaom `av1/common/restoration.c`, `av1/common/restoration.h`, `av1/common/convolve.c`, and `av1/common/convolve.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve the implicit center-sample contribution, separable horizontal/vertical rounding, bit-depth-dependent 16-bit intermediate range, and final 8/10/12-bit clipping. Reuse `Vector128_.MultiplyAddAdjacent` for the contiguous horizontal eight-tap product with an exact scalar fallback. Keep this scalar/SIMD oracle disabled until restoration stripe boundaries and self-guided filtering are both complete. | | `Av1SelfGuidedFilter` | AV1 sections 7.17.2 and 7.17.3 self-guided and box-filter processes | libaom `av1/common/restoration.c` and `av1/common/restoration.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve the sixteen normative radius/variance parameter sets, local mean and variance normalization, alternating-row radius-two optimization, decoded projection-coefficient behavior, signed rounding, and 8/10/12-bit clipping. Use caller-owned scratch storage and a scalar oracle before considering a managed SIMD translation of libaom's architecture-specific nonlinear filter kernels. Keep this image-reconstruction stage disabled until restoration stripe boundaries are complete. | | `Av1LoopRestorationBoundary`, `Av1LoopRestorationDecoder`, and `Av1FrameDecoder` restoration-stage ordering | AV1 section 7.17 loop restoration, including striped boundary semantics | libaom `av1/common/restoration.c`, `av1/common/restoration.h`, `av1/common/resize.c`, and `av1/decoder/decodeframe.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve two deblocked rows at internal 64-luma stripe boundaries before CDEF, apply the existing normative SIMD-backed super-resolution kernel to saved rows when scaled, use post-CDEF/super-resolution samples at frame edges, extend the final restoration unit up to 150 percent of nominal size, and filter from immutable plane snapshots into separate output planes. This is bounded still-image reconstruction state, not retained reference-frame, track, timing, or playback state. | +| `Av1FilmGrainDecoder` and `Av1FilmGrainGaussianSequence` | AV1 section 7.18 film-grain synthesis | libaom `av1/decoder/grain_synthesis.c`, `av1/decoder/grain_synthesis.h`, and `aom_dsp/grain_params.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve the normative 2,048-sample Gaussian sequence, linear-feedback shift register, luma/chroma autoregressive templates, scaling lookup interpolation, 32x32 block selection, boundary overlap, restricted-range clipping, monochrome and 4:2:0/4:2:2/4:4:4 layouts, and 8/10/12-bit arithmetic. Use allocator-owned scratch and runtime-optimized span copies. Apply grain only to the displayed still-image samples after all in-loop filters; reference-frame parameter inheritance remains sequence-playback state and is outside this codec scope. | | `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. | @@ -125,10 +126,10 @@ This assessment is based on the current source after the upstream ImageSharp mer ### AV1 decoder - The single-still `Av1Decoder` path now parses tile state before allocating and reconstructing one independently decodable frame, and it disposes the reconstruction planes after pixel conversion. It deliberately does not retain animation/video reference frames or implement `show_existing_frame` playback state. -- The reconstruction pipeline now records plane-relative transform geometry, preserves tile-local delta-Q and delta-LF predictors, derives segmentation and reference-adjusted filter levels, and runs the exact scalar low/high-bit-depth AV1 4-, 6-, 8-, and 14-tap deblocking kernels in normative vertical-then-horizontal order. It then applies scalar CDEF direction search, luma variance adjustment, primary and secondary constrained taps, chroma direction conversion, high-bit-depth scaling, skipped-block selection, and frame-edge sentinel handling from immutable per-plane snapshots. 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 now 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. Independent 8-, 10-, and 12-bit AVIF vectors exercising every active filter 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 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. - 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 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. -- 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. +- 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. Chroma-from-luma storage, subsampling, parameter derivation, U/V sharing, and 8/10/12-bit prediction are active; independently encoded high-bit-depth and chroma-from-luma AVIF conformance files are still required. - `Av1YuvConverter` now consumes the signaled range, supported H.273 matrix coefficients, subsampling, and chroma sample position for 8, 10, and 12-bit output. Its high-bit-depth decode and encode paths use allocator-backed `Rgb48` rows and the existing `PixelOperations` conversions, avoiding the former eight-bit intermediate. Encoder conversion covers monochrome, YUV 4:2:0, 4:2:2, and 4:4:4 with libavif-compatible box averaging. Constant-luminance and chromaticity-derived matrices and ICtCp remain incomplete. - The inverse-transform path allocates arrays in a per-transform hot path. - The production prediction, transform, and nonlinear self-guided paths remain predominantly scalar. Normative super-resolution and Wiener horizontal products now reuse ImageSharp's cross-platform adjacent multiply/add SIMD helper with exact scalar fallbacks; further SIMD work must preserve these scalar reconstruction oracles. diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs index 90be8a5c6c..eef47bd2c8 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs @@ -66,11 +66,6 @@ internal class ObuReader case ObuType.SequenceHeader: this.SequenceHeader = new(); ReadSequenceHeader(ref reader, this.SequenceHeader); - if (this.SequenceHeader.ColorConfig.BitDepth == Av1BitDepth.TwelveBit) - { - // TODO: Initialize 12 bit predictors - } - break; case ObuType.FrameHeader: case ObuType.RedundantFrameHeader: diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs index 3eac8f267a..8b187267d3 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.FilmGrain; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantification; @@ -126,8 +127,13 @@ internal class Av1FrameDecoder : IAv1FrameDecoder loopRestorationDecoder.DecodeFrame(); } - // A still-image decode ends at the visible restored samples. Extending reference-frame - // borders is sequence playback state and is deliberately outside this decoder's scope. + // Film grain belongs to the displayed image rather than the reference reconstruction, so it + // follows every in-loop filter. This decoder owns no retained reference frames. + Av1FilmGrainDecoder filmGrainDecoder = new(this.sequenceHeader, this.frameHeader, this.frameBuffer); + filmGrainDecoder.DecodeFrame(); + + // Extending reference-frame borders is sequence playback state and is deliberately outside + // this still-image decoder's scope. } /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainDecoder.cs new file mode 100644 index 0000000000..c34100171f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainDecoder.cs @@ -0,0 +1,1498 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.FilmGrain; + +/// +/// Synthesizes the film-grain signal carried by one independently decodable AV1 still-image frame. +/// +internal sealed class Av1FilmGrainDecoder +{ + /// + /// The luma width and height of one independently selected grain block. + /// + private const int LumaSubblockSize = 32; + + /// + /// The maximum autoregressive lag reserved around generated grain templates. + /// + private const int AutoregressivePadding = 3; + + /// + /// The template padding that precedes the autoregressive stabilization region. + /// + private const int TemplatePadding = 3; + + /// + /// The number of random bits used to index the normative Gaussian sequence. + /// + private const int GaussianIndexBits = 11; + + /// + /// The lower restricted-range luma value at eight-bit precision. + /// + private const int RestrictedLumaMinimum = 16; + + /// + /// The upper restricted-range luma value at eight-bit precision. + /// + private const int RestrictedLumaMaximum = 235; + + /// + /// The lower restricted-range chroma value at eight-bit precision. + /// + private const int RestrictedChromaMinimum = 16; + + /// + /// The upper restricted-range chroma value at eight-bit precision. + /// + private const int RestrictedChromaMaximum = 240; + + /// + /// The sequence-level bit-depth and chroma-sampling configuration. + /// + private readonly ObuSequenceHeader sequenceHeader; + + /// + /// The frame-level film-grain synthesis parameters. + /// + private readonly ObuFrameHeader frameHeader; + + /// + /// The restored planar samples that receive the synthesized grain. + /// + private readonly Av1FrameBuffer frameBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The sequence header defining bit depth and chroma sampling. + /// The frame header containing the complete still-frame grain parameters. + /// The restored frame samples to which grain is added. + public Av1FilmGrainDecoder( + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1FrameBuffer frameBuffer) + { + this.sequenceHeader = sequenceHeader; + this.frameHeader = frameHeader; + this.frameBuffer = frameBuffer; + } + + /// + /// Adds the signaled film grain to the visible luma and chroma samples. + /// + public void DecodeFrame() + { + ObuFilmGrainParameters parameters = this.frameHeader.FilmGrainParameters; + if (!parameters.ApplyGrain) + { + return; + } + + ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig; + bool isMonochrome = colorConfig.IsMonochrome; + int subsamplingX = !isMonochrome && colorConfig.SubSamplingX ? 1 : 0; + int subsamplingY = !isMonochrome && colorConfig.SubSamplingY ? 1 : 0; + int visibleWidth = this.frameBuffer.Width; + int visibleHeight = this.frameBuffer.Height; + int alignedWidth = Av1Math.AlignPowerOf2(visibleWidth, 1); + int alignedHeight = Av1Math.AlignPowerOf2(visibleHeight, 1); + + Buffer2D lumaBuffer = this.frameBuffer.BufferY!; + int lumaStride = lumaBuffer.Width / this.frameBuffer.BytesPerSample; + int chromaStride = isMonochrome + ? 0 + : this.frameBuffer.BufferCb!.Width / this.frameBuffer.BytesPerSample; + + if (this.frameBuffer.BytesPerSample == 2) + { + Span luma = GetPlaneSamples( + lumaBuffer, + this.frameBuffer.OriginX, + this.frameBuffer.OriginY); + + Span cb = isMonochrome + ? Span.Empty + : GetPlaneSamples( + this.frameBuffer.BufferCb!, + this.frameBuffer.OriginX >> subsamplingX, + this.frameBuffer.OriginY >> subsamplingY); + + Span cr = isMonochrome + ? Span.Empty + : GetPlaneSamples( + this.frameBuffer.BufferCr!, + this.frameBuffer.OriginX >> subsamplingX, + this.frameBuffer.OriginY >> subsamplingY); + + ExtendPlane(luma, lumaStride, visibleWidth, visibleHeight, alignedWidth, alignedHeight); + if (!isMonochrome) + { + int visibleChromaWidth = Av1Math.DivideLog2Ceiling(visibleWidth, subsamplingX); + int visibleChromaHeight = Av1Math.DivideLog2Ceiling(visibleHeight, subsamplingY); + int alignedChromaWidth = alignedWidth >> subsamplingX; + int alignedChromaHeight = alignedHeight >> subsamplingY; + ExtendPlane(cb, chromaStride, visibleChromaWidth, visibleChromaHeight, alignedChromaWidth, alignedChromaHeight); + ExtendPlane(cr, chromaStride, visibleChromaWidth, visibleChromaHeight, alignedChromaWidth, alignedChromaHeight); + } + + this.ApplyGrain( + parameters, + luma, + cb, + cr, + alignedWidth, + alignedHeight, + lumaStride, + chromaStride, + subsamplingX, + subsamplingY, + isMonochrome); + } + else + { + Span luma = GetPlaneSamples( + lumaBuffer, + this.frameBuffer.OriginX, + this.frameBuffer.OriginY); + + Span cb = isMonochrome + ? Span.Empty + : GetPlaneSamples( + this.frameBuffer.BufferCb!, + this.frameBuffer.OriginX >> subsamplingX, + this.frameBuffer.OriginY >> subsamplingY); + + Span cr = isMonochrome + ? Span.Empty + : GetPlaneSamples( + this.frameBuffer.BufferCr!, + this.frameBuffer.OriginX >> subsamplingX, + this.frameBuffer.OriginY >> subsamplingY); + + ExtendPlane(luma, lumaStride, visibleWidth, visibleHeight, alignedWidth, alignedHeight); + if (!isMonochrome) + { + int visibleChromaWidth = Av1Math.DivideLog2Ceiling(visibleWidth, subsamplingX); + int visibleChromaHeight = Av1Math.DivideLog2Ceiling(visibleHeight, subsamplingY); + int alignedChromaWidth = alignedWidth >> subsamplingX; + int alignedChromaHeight = alignedHeight >> subsamplingY; + ExtendPlane(cb, chromaStride, visibleChromaWidth, visibleChromaHeight, alignedChromaWidth, alignedChromaHeight); + ExtendPlane(cr, chromaStride, visibleChromaWidth, visibleChromaHeight, alignedChromaWidth, alignedChromaHeight); + } + + this.ApplyGrain( + parameters, + luma, + cb, + cr, + alignedWidth, + alignedHeight, + lumaStride, + chromaStride, + subsamplingX, + subsamplingY, + isMonochrome); + } + } + + /// + /// Gets a plane span beginning at its first visible sample. + /// + /// The native eight-bit or high-bit-depth sample type. + /// The padded plane allocation. + /// The horizontal visible origin in samples. + /// The vertical visible origin in rows. + /// The sample span beginning at the visible origin and retaining the padded row stride. + private static Span GetPlaneSamples(Buffer2D buffer, int originX, int originY) + where TSample : unmanaged + { + Span samples = MemoryMarshal.Cast(buffer.DangerousGetSingleSpan()); + int stride = buffer.Width / Unsafe.SizeOf(); + return samples[((originY * stride) + originX)..]; + } + + /// + /// Replicates the last visible row and column when film-grain block traversal requires even dimensions. + /// + /// The native eight-bit or high-bit-depth sample type. + /// The plane span beginning at its visible origin. + /// The number of samples between adjacent rows. + /// The visible plane width. + /// The visible plane height. + /// The even width consumed by synthesis. + /// The even height consumed by synthesis. + private static void ExtendPlane( + Span plane, + int stride, + int visibleWidth, + int visibleHeight, + int alignedWidth, + int alignedHeight) + where TSample : unmanaged + { + if (visibleWidth != alignedWidth) + { + for (int row = 0; row < visibleHeight; row++) + { + int rowOffset = row * stride; + plane[rowOffset + visibleWidth] = plane[rowOffset + visibleWidth - 1]; + } + } + + if (visibleHeight != alignedHeight) + { + // Span.CopyTo uses the runtime's optimized bulk-copy path and preserves the already replicated edge column. + plane.Slice((visibleHeight - 1) * stride, alignedWidth) + .CopyTo(plane.Slice(visibleHeight * stride, alignedWidth)); + } + } + + /// + /// Generates reusable grain templates and adds selected blocks to one restored frame. + /// + /// The native eight-bit or high-bit-depth sample type. + /// The complete self-contained frame grain parameters. + /// The luma plane beginning at its visible origin. + /// The first chroma plane, or an empty span for monochrome input. + /// The second chroma plane, or an empty span for monochrome input. + /// The even luma width processed by synthesis. + /// The even luma height processed by synthesis. + /// The luma row stride in samples. + /// The chroma row stride in samples. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// Whether the frame has no chroma planes. + private void ApplyGrain( + ObuFilmGrainParameters parameters, + Span luma, + Span cb, + Span cr, + int width, + int height, + int lumaStride, + int chromaStride, + int subsamplingX, + int subsamplingY, + bool isMonochrome) + where TSample : unmanaged + { + int chromaSubblockHeight = LumaSubblockSize >> subsamplingY; + int chromaSubblockWidth = LumaSubblockSize >> subsamplingX; + int lumaBlockHeight = TemplatePadding + (2 * AutoregressivePadding) + (2 * LumaSubblockSize); + int lumaBlockWidth = TemplatePadding + (2 * AutoregressivePadding) + (2 * LumaSubblockSize) + + (2 * AutoregressivePadding) + TemplatePadding; + + int chromaBlockHeight = TemplatePadding + ((2 >> subsamplingY) * AutoregressivePadding) + + (2 * chromaSubblockHeight); + + int chromaBlockWidth = TemplatePadding + ((2 >> subsamplingX) * AutoregressivePadding) + + (2 * chromaSubblockWidth) + ((2 >> subsamplingX) * AutoregressivePadding) + TemplatePadding; + + int lumaGrainLength = lumaBlockHeight * lumaBlockWidth; + int chromaGrainLength = isMonochrome ? 0 : chromaBlockHeight * chromaBlockWidth; + int scalingLength = isMonochrome ? 256 : 768; + int lumaLineLength = parameters.OverlapFlag ? lumaStride * 2 : 0; + int chromaLineLength = parameters.OverlapFlag && !isMonochrome + ? chromaStride * (2 >> subsamplingY) + : 0; + + int lumaColumnLength = parameters.OverlapFlag ? (LumaSubblockSize + 2) * 2 : 0; + int chromaColumnLength = parameters.OverlapFlag && !isMonochrome + ? (chromaSubblockHeight + (2 >> subsamplingY)) * (2 >> subsamplingX) + : 0; + + int scratchLength = scalingLength + lumaGrainLength + (2 * chromaGrainLength) + + lumaLineLength + (2 * chromaLineLength) + lumaColumnLength + (2 * chromaColumnLength); + + using IMemoryOwner scratchOwner = this.frameBuffer.MemoryAllocator.Allocate(scratchLength); + Span scratch = scratchOwner.GetSpan()[..scratchLength]; + int scratchOffset = 0; + + Span scalingY = scratch.Slice(scratchOffset, 256); + scratchOffset += 256; + Span scalingCb = isMonochrome ? Span.Empty : scratch.Slice(scratchOffset, 256); + scratchOffset += scalingCb.Length; + Span scalingCr = isMonochrome ? Span.Empty : scratch.Slice(scratchOffset, 256); + scratchOffset += scalingCr.Length; + Span lumaGrain = scratch.Slice(scratchOffset, lumaGrainLength); + scratchOffset += lumaGrain.Length; + Span cbGrain = isMonochrome ? Span.Empty : scratch.Slice(scratchOffset, chromaGrainLength); + scratchOffset += cbGrain.Length; + Span crGrain = isMonochrome ? Span.Empty : scratch.Slice(scratchOffset, chromaGrainLength); + scratchOffset += crGrain.Length; + Span yLineBuffer = parameters.OverlapFlag ? scratch.Slice(scratchOffset, lumaLineLength) : Span.Empty; + scratchOffset += yLineBuffer.Length; + Span cbLineBuffer = parameters.OverlapFlag && !isMonochrome + ? scratch.Slice(scratchOffset, chromaLineLength) + : Span.Empty; + + scratchOffset += cbLineBuffer.Length; + Span crLineBuffer = parameters.OverlapFlag && !isMonochrome + ? scratch.Slice(scratchOffset, chromaLineLength) + : Span.Empty; + + scratchOffset += crLineBuffer.Length; + Span yColumnBuffer = parameters.OverlapFlag + ? scratch.Slice(scratchOffset, lumaColumnLength) + : Span.Empty; + + scratchOffset += yColumnBuffer.Length; + Span cbColumnBuffer = parameters.OverlapFlag && !isMonochrome + ? scratch.Slice(scratchOffset, chromaColumnLength) + : Span.Empty; + + scratchOffset += cbColumnBuffer.Length; + Span crColumnBuffer = parameters.OverlapFlag && !isMonochrome + ? scratch.Slice(scratchOffset, chromaColumnLength) + : Span.Empty; + + // libaom zero-initializes the lookup structure before expanding control points. This matters + // when chroma scaling is inherited from an empty luma scaling function. + scalingY.Clear(); + scalingCb.Clear(); + scalingCr.Clear(); + + int bitDepth = this.sequenceHeader.ColorConfig.BitDepth.GetBitCount(); + ushort randomRegister = (ushort)parameters.GrainSeed; + GenerateLumaGrain( + parameters, + ref randomRegister, + lumaGrain, + lumaBlockHeight, + lumaBlockWidth, + lumaBlockWidth, + bitDepth); + + if (!isMonochrome) + { + GenerateChromaGrain( + parameters, + ref randomRegister, + lumaGrain, + cbGrain, + crGrain, + lumaBlockWidth, + chromaBlockHeight, + chromaBlockWidth, + chromaBlockWidth, + subsamplingX, + subsamplingY, + bitDepth); + } + + InitializeScalingFunction(parameters.PointYValue, parameters.PointYScaling, (int)parameters.NumYPoints, scalingY); + if (!isMonochrome) + { + if (parameters.ChromaScalingFromLuma) + { + scalingY.CopyTo(scalingCb); + scalingY.CopyTo(scalingCr); + } + else + { + InitializeScalingFunction( + parameters.PointCbValue, + parameters.PointCbScaling, + (int)parameters.NumCbPoints, + scalingCb); + + InitializeScalingFunction( + parameters.PointCrValue, + parameters.PointCrScaling, + (int)parameters.NumCrPoints, + scalingCr); + } + } + + int grainMinimum = -(1 << (bitDepth - 1)); + int grainMaximum = (1 << (bitDepth - 1)) - 1; + bool isIdentityMatrix = this.sequenceHeader.ColorConfig.MatrixCoefficients == ObuMatrixCoefficients.Identity; + for (int halfY = 0; halfY < height / 2; halfY += LumaSubblockSize >> 1) + { + InitializeRandomGenerator(ref randomRegister, halfY << 1, (ushort)parameters.GrainSeed); + + for (int halfX = 0; halfX < width / 2; halfX += LumaSubblockSize >> 1) + { + int randomOffset = GetRandomNumber(ref randomRegister, 8); + int offsetX = (randomOffset >> 4) & 15; + int offsetY = randomOffset & 15; + int lumaOffsetY = TemplatePadding + (2 * AutoregressivePadding) + (offsetY << 1); + int lumaOffsetX = TemplatePadding + (2 * AutoregressivePadding) + (offsetX << 1); + int chromaOffsetY = TemplatePadding + ((2 >> subsamplingY) * AutoregressivePadding) + + (offsetY * (2 >> subsamplingY)); + + int chromaOffsetX = TemplatePadding + ((2 >> subsamplingX) * AutoregressivePadding) + + (offsetX * (2 >> subsamplingX)); + + if (parameters.OverlapFlag && halfX != 0) + { + VerticalOverlap( + yColumnBuffer, + 2, + lumaGrain[((lumaOffsetY * lumaBlockWidth) + lumaOffsetX)..], + lumaBlockWidth, + yColumnBuffer, + 2, + 2, + Math.Min(LumaSubblockSize + 2, height - (halfY << 1)), + grainMinimum, + grainMaximum); + + if (!isMonochrome) + { + int chromaOverlapWidth = 2 >> subsamplingX; + int chromaOverlapHeight = Math.Min( + chromaSubblockHeight + (2 >> subsamplingY), + (height - (halfY << 1)) >> subsamplingY); + + VerticalOverlap( + cbColumnBuffer, + chromaOverlapWidth, + cbGrain[((chromaOffsetY * chromaBlockWidth) + chromaOffsetX)..], + chromaBlockWidth, + cbColumnBuffer, + chromaOverlapWidth, + chromaOverlapWidth, + chromaOverlapHeight, + grainMinimum, + grainMaximum); + + VerticalOverlap( + crColumnBuffer, + chromaOverlapWidth, + crGrain[((chromaOffsetY * chromaBlockWidth) + chromaOffsetX)..], + chromaBlockWidth, + crColumnBuffer, + chromaOverlapWidth, + chromaOverlapWidth, + chromaOverlapHeight, + grainMinimum, + grainMaximum); + } + + int rowAdjustment = halfY != 0 ? 1 : 0; + int destinationLumaOffset = (((halfY + rowAdjustment) << 1) * lumaStride) + (halfX << 1); + int destinationChromaOffset = (((halfY + rowAdjustment) << (1 - subsamplingY)) * chromaStride) + + (halfX << (1 - subsamplingX)); + + int columnGrainOffset = rowAdjustment * (2 - subsamplingY) * (2 - subsamplingX); + Span destinationCb = isMonochrome + ? Span.Empty + : cb[destinationChromaOffset..]; + + Span destinationCr = isMonochrome + ? Span.Empty + : cr[destinationChromaOffset..]; + + Span columnCbGrain = isMonochrome + ? Span.Empty + : cbColumnBuffer[columnGrainOffset..]; + + Span columnCrGrain = isMonochrome + ? Span.Empty + : crColumnBuffer[columnGrainOffset..]; + + AddNoiseToBlock( + parameters, + scalingY, + scalingCb, + scalingCr, + luma[destinationLumaOffset..], + destinationCb, + destinationCr, + lumaStride, + chromaStride, + yColumnBuffer[(rowAdjustment * 4)..], + columnCbGrain, + columnCrGrain, + 2, + 2 - subsamplingX, + Math.Min(LumaSubblockSize >> 1, (height / 2) - halfY) - rowAdjustment, + 1, + bitDepth, + subsamplingX, + subsamplingY, + isMonochrome, + isIdentityMatrix); + } + + if (parameters.OverlapFlag && halfY != 0) + { + if (halfX != 0) + { + HorizontalOverlap( + yLineBuffer[(halfX << 1)..], + lumaStride, + yColumnBuffer, + 2, + yLineBuffer[(halfX << 1)..], + lumaStride, + 2, + 2, + grainMinimum, + grainMaximum); + + if (!isMonochrome) + { + int chromaOverlapWidth = 2 >> subsamplingX; + int chromaOverlapHeight = 2 >> subsamplingY; + int chromaLineOffset = halfX * chromaOverlapWidth; + HorizontalOverlap( + cbLineBuffer[chromaLineOffset..], + chromaStride, + cbColumnBuffer, + chromaOverlapWidth, + cbLineBuffer[chromaLineOffset..], + chromaStride, + chromaOverlapWidth, + chromaOverlapHeight, + grainMinimum, + grainMaximum); + + HorizontalOverlap( + crLineBuffer[chromaLineOffset..], + chromaStride, + crColumnBuffer, + chromaOverlapWidth, + crLineBuffer[chromaLineOffset..], + chromaStride, + chromaOverlapWidth, + chromaOverlapHeight, + grainMinimum, + grainMaximum); + } + } + + int overlappedColumn = halfX != 0 ? halfX + 1 : 0; + int templateColumnAdjustment = halfX != 0 ? 2 : 0; + int horizontalWidth = Math.Min( + LumaSubblockSize - templateColumnAdjustment, + width - (overlappedColumn << 1)); + + HorizontalOverlap( + yLineBuffer[(overlappedColumn << 1)..], + lumaStride, + lumaGrain[((lumaOffsetY * lumaBlockWidth) + lumaOffsetX + templateColumnAdjustment)..], + lumaBlockWidth, + yLineBuffer[(overlappedColumn << 1)..], + lumaStride, + horizontalWidth, + 2, + grainMinimum, + grainMaximum); + + if (!isMonochrome) + { + int chromaColumnAdjustment = halfX != 0 ? 2 >> subsamplingX : 0; + int chromaDestinationOffset = overlappedColumn << (1 - subsamplingX); + int chromaWidth = Math.Min( + chromaSubblockWidth - chromaColumnAdjustment, + (width - (overlappedColumn << 1)) >> subsamplingX); + + HorizontalOverlap( + cbLineBuffer[chromaDestinationOffset..], + chromaStride, + cbGrain[((chromaOffsetY * chromaBlockWidth) + chromaOffsetX + chromaColumnAdjustment)..], + chromaBlockWidth, + cbLineBuffer[chromaDestinationOffset..], + chromaStride, + chromaWidth, + 2 >> subsamplingY, + grainMinimum, + grainMaximum); + + HorizontalOverlap( + crLineBuffer[chromaDestinationOffset..], + chromaStride, + crGrain[((chromaOffsetY * chromaBlockWidth) + chromaOffsetX + chromaColumnAdjustment)..], + chromaBlockWidth, + crLineBuffer[chromaDestinationOffset..], + chromaStride, + chromaWidth, + 2 >> subsamplingY, + grainMinimum, + grainMaximum); + } + + int boundaryLumaOffset = ((halfY << 1) * lumaStride) + (halfX << 1); + int boundaryChromaOffset = ((halfY << (1 - subsamplingY)) * chromaStride) + + (halfX << (1 - subsamplingX)); + + Span boundaryCb = isMonochrome + ? Span.Empty + : cb[boundaryChromaOffset..]; + + Span boundaryCr = isMonochrome + ? Span.Empty + : cr[boundaryChromaOffset..]; + + Span boundaryCbGrain = isMonochrome + ? Span.Empty + : cbLineBuffer[(halfX << (1 - subsamplingX))..]; + + Span boundaryCrGrain = isMonochrome + ? Span.Empty + : crLineBuffer[(halfX << (1 - subsamplingX))..]; + + AddNoiseToBlock( + parameters, + scalingY, + scalingCb, + scalingCr, + luma[boundaryLumaOffset..], + boundaryCb, + boundaryCr, + lumaStride, + chromaStride, + yLineBuffer[(halfX << 1)..], + boundaryCbGrain, + boundaryCrGrain, + lumaStride, + chromaStride, + 1, + Math.Min(LumaSubblockSize >> 1, (width / 2) - halfX), + bitDepth, + subsamplingX, + subsamplingY, + isMonochrome, + isIdentityMatrix); + } + + int interiorRowAdjustment = parameters.OverlapFlag && halfY != 0 ? 1 : 0; + int interiorColumnAdjustment = parameters.OverlapFlag && halfX != 0 ? 1 : 0; + int lumaGrainOffset = ((lumaOffsetY + (interiorRowAdjustment << 1)) * lumaBlockWidth) + + lumaOffsetX + (interiorColumnAdjustment << 1); + + int chromaGrainOffset = ((chromaOffsetY + + (interiorRowAdjustment << (1 - subsamplingY))) * chromaBlockWidth) + + chromaOffsetX + (interiorColumnAdjustment << (1 - subsamplingX)); + + int interiorLumaOffset = (((halfY + interiorRowAdjustment) << 1) * lumaStride) + + ((halfX + interiorColumnAdjustment) << 1); + + int interiorChromaOffset = (((halfY + interiorRowAdjustment) << (1 - subsamplingY)) * chromaStride) + + ((halfX + interiorColumnAdjustment) << (1 - subsamplingX)); + + Span interiorCb = isMonochrome ? Span.Empty : cb[interiorChromaOffset..]; + Span interiorCr = isMonochrome ? Span.Empty : cr[interiorChromaOffset..]; + Span interiorCbGrain = isMonochrome ? Span.Empty : cbGrain[chromaGrainOffset..]; + Span interiorCrGrain = isMonochrome ? Span.Empty : crGrain[chromaGrainOffset..]; + + AddNoiseToBlock( + parameters, + scalingY, + scalingCb, + scalingCr, + luma[interiorLumaOffset..], + interiorCb, + interiorCr, + lumaStride, + chromaStride, + lumaGrain[lumaGrainOffset..], + interiorCbGrain, + interiorCrGrain, + lumaBlockWidth, + chromaBlockWidth, + Math.Min(LumaSubblockSize >> 1, (height / 2) - halfY) - interiorRowAdjustment, + Math.Min(LumaSubblockSize >> 1, (width / 2) - halfX) - interiorColumnAdjustment, + bitDepth, + subsamplingX, + subsamplingY, + isMonochrome, + isIdentityMatrix); + + if (parameters.OverlapFlag) + { + if (halfX != 0) + { + CopyArea( + yColumnBuffer[(LumaSubblockSize << 1)..], + 2, + yLineBuffer[(halfX << 1)..], + lumaStride, + 2, + 2); + + if (!isMonochrome) + { + int chromaOverlapWidth = 2 >> subsamplingX; + int chromaOverlapHeight = 2 >> subsamplingY; + int sourceOffset = chromaSubblockHeight << (1 - subsamplingX); + int destinationOffset = halfX << (1 - subsamplingX); + CopyArea( + cbColumnBuffer[sourceOffset..], + chromaOverlapWidth, + cbLineBuffer[destinationOffset..], + chromaStride, + chromaOverlapWidth, + chromaOverlapHeight); + + CopyArea( + crColumnBuffer[sourceOffset..], + chromaOverlapWidth, + crLineBuffer[destinationOffset..], + chromaStride, + chromaOverlapWidth, + chromaOverlapHeight); + } + } + + int lineDestinationColumn = halfX != 0 ? halfX + 1 : 0; + int lineTemplateAdjustment = halfX != 0 ? 2 : 0; + int lineWidth = Math.Min(LumaSubblockSize, width - (halfX << 1)) - lineTemplateAdjustment; + CopyArea( + lumaGrain[(((lumaOffsetY + LumaSubblockSize) * lumaBlockWidth) + + lumaOffsetX + lineTemplateAdjustment)..], + lumaBlockWidth, + yLineBuffer[(lineDestinationColumn << 1)..], + lumaStride, + lineWidth, + 2); + + if (!isMonochrome) + { + int chromaLineAdjustment = halfX != 0 ? 2 >> subsamplingX : 0; + int chromaLineWidth = Math.Min( + chromaSubblockWidth, + (width - (halfX << 1)) >> subsamplingX) - chromaLineAdjustment; + + int chromaLineDestination = lineDestinationColumn << (1 - subsamplingX); + CopyArea( + cbGrain[(((chromaOffsetY + chromaSubblockHeight) * chromaBlockWidth) + + chromaOffsetX + chromaLineAdjustment)..], + chromaBlockWidth, + cbLineBuffer[chromaLineDestination..], + chromaStride, + chromaLineWidth, + 2 >> subsamplingY); + + CopyArea( + crGrain[(((chromaOffsetY + chromaSubblockHeight) * chromaBlockWidth) + + chromaOffsetX + chromaLineAdjustment)..], + chromaBlockWidth, + crLineBuffer[chromaLineDestination..], + chromaStride, + chromaLineWidth, + 2 >> subsamplingY); + } + + CopyArea( + lumaGrain[((lumaOffsetY * lumaBlockWidth) + lumaOffsetX + LumaSubblockSize)..], + lumaBlockWidth, + yColumnBuffer, + 2, + 2, + Math.Min(LumaSubblockSize + 2, height - (halfY << 1))); + + if (!isMonochrome) + { + int chromaOverlapWidth = 2 >> subsamplingX; + int chromaOverlapHeight = Math.Min( + chromaSubblockHeight + (2 >> subsamplingY), + (height - (halfY << 1)) >> subsamplingY); + + CopyArea( + cbGrain[((chromaOffsetY * chromaBlockWidth) + + chromaOffsetX + chromaSubblockWidth)..], + chromaBlockWidth, + cbColumnBuffer, + chromaOverlapWidth, + chromaOverlapWidth, + chromaOverlapHeight); + + CopyArea( + crGrain[((chromaOffsetY * chromaBlockWidth) + + chromaOffsetX + chromaSubblockWidth)..], + chromaBlockWidth, + crColumnBuffer, + chromaOverlapWidth, + chromaOverlapWidth, + chromaOverlapHeight); + } + } + } + } + } + + /// + /// Generates the luma grain template and applies its autoregressive filter. + /// + /// The frame grain parameters. + /// The pseudo-random register advanced while filling the template. + /// The luma grain template. + /// The template height. + /// The template width. + /// The template row stride. + /// The decoded sample bit depth. + private static void GenerateLumaGrain( + ObuFilmGrainParameters parameters, + ref ushort randomRegister, + Span grain, + int height, + int width, + int stride, + int bitDepth) + { + if (parameters.NumYPoints == 0) + { + grain.Clear(); + return; + } + + int gaussianShift = 12 - bitDepth + (int)parameters.GrainScaleShift; + int gaussianRounding = (1 << gaussianShift) >> 1; + ReadOnlySpan gaussian = Av1FilmGrainGaussianSequence.Samples; + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int randomIndex = GetRandomNumber(ref randomRegister, GaussianIndexBits); + grain[(row * stride) + column] = (gaussian[randomIndex] + gaussianRounding) >> gaussianShift; + } + } + + int lag = (int)parameters.ArCoeffLag; + int coefficientShift = (int)parameters.ArCoeffShiftMinus6 + 6; + int roundingOffset = 1 << (coefficientShift - 1); + int grainMinimum = -(1 << (bitDepth - 1)); + int grainMaximum = (1 << (bitDepth - 1)) - 1; + uint[] coefficients = parameters.ArCoeffsYPlus128!; + for (int row = TemplatePadding; row < height; row++) + { + for (int column = TemplatePadding; column < width - TemplatePadding; column++) + { + int weightedSum = 0; + int coefficientIndex = 0; + + // The AV1 coefficient order walks the complete rows above the current sample before + // the already generated samples to its left on the current row. + for (int relativeRow = -lag; relativeRow < 0; relativeRow++) + { + for (int relativeColumn = -lag; relativeColumn <= lag; relativeColumn++) + { + weightedSum += ((int)coefficients[coefficientIndex++] - 128) * + grain[((row + relativeRow) * stride) + column + relativeColumn]; + } + } + + for (int relativeColumn = -lag; relativeColumn < 0; relativeColumn++) + { + weightedSum += ((int)coefficients[coefficientIndex++] - 128) * + grain[(row * stride) + column + relativeColumn]; + } + + int grainIndex = (row * stride) + column; + grain[grainIndex] = Av1Math.Clamp( + grain[grainIndex] + ((weightedSum + roundingOffset) >> coefficientShift), + grainMinimum, + grainMaximum); + } + } + } + + /// + /// Generates both chroma grain templates and applies their autoregressive filters. + /// + /// The frame grain parameters. + /// The pseudo-random register used for chroma template generation. + /// The completed luma grain template. + /// The first chroma grain template. + /// The second chroma grain template. + /// The luma grain-template row stride. + /// The chroma template height. + /// The chroma template width. + /// The chroma template row stride. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// The decoded sample bit depth. + private static void GenerateChromaGrain( + ObuFilmGrainParameters parameters, + ref ushort randomRegister, + ReadOnlySpan lumaGrain, + Span cbGrain, + Span crGrain, + int lumaStride, + int height, + int width, + int stride, + int subsamplingX, + int subsamplingY, + int bitDepth) + { + bool applyCb = parameters.NumCbPoints != 0 || parameters.ChromaScalingFromLuma; + bool applyCr = parameters.NumCrPoints != 0 || parameters.ChromaScalingFromLuma; + int gaussianShift = 12 - bitDepth + (int)parameters.GrainScaleShift; + int gaussianRounding = (1 << gaussianShift) >> 1; + ReadOnlySpan gaussian = Av1FilmGrainGaussianSequence.Samples; + if (applyCb) + { + InitializeRandomGenerator(ref randomRegister, 7 << 5, (ushort)parameters.GrainSeed); + FillGaussianGrain(ref randomRegister, cbGrain, height, width, stride, gaussian, gaussianShift, gaussianRounding); + } + else + { + cbGrain.Clear(); + } + + if (applyCr) + { + InitializeRandomGenerator(ref randomRegister, 11 << 5, (ushort)parameters.GrainSeed); + FillGaussianGrain(ref randomRegister, crGrain, height, width, stride, gaussian, gaussianShift, gaussianRounding); + } + else + { + crGrain.Clear(); + } + + int lag = (int)parameters.ArCoeffLag; + int coefficientShift = (int)parameters.ArCoeffShiftMinus6 + 6; + int roundingOffset = 1 << (coefficientShift - 1); + int grainMinimum = -(1 << (bitDepth - 1)); + int grainMaximum = (1 << (bitDepth - 1)) - 1; + uint[]? cbCoefficients = parameters.ArCoeffsCbPlus128; + uint[]? crCoefficients = parameters.ArCoeffsCrPlus128; + for (int row = TemplatePadding; row < height; row++) + { + for (int column = TemplatePadding; column < width - TemplatePadding; column++) + { + int weightedCb = 0; + int weightedCr = 0; + int coefficientIndex = 0; + for (int relativeRow = -lag; relativeRow < 0; relativeRow++) + { + for (int relativeColumn = -lag; relativeColumn <= lag; relativeColumn++) + { + int grainIndex = ((row + relativeRow) * stride) + column + relativeColumn; + if (applyCb) + { + weightedCb += ((int)cbCoefficients![coefficientIndex] - 128) * cbGrain[grainIndex]; + } + + if (applyCr) + { + weightedCr += ((int)crCoefficients![coefficientIndex] - 128) * crGrain[grainIndex]; + } + + coefficientIndex++; + } + } + + for (int relativeColumn = -lag; relativeColumn < 0; relativeColumn++) + { + int grainIndex = (row * stride) + column + relativeColumn; + if (applyCb) + { + weightedCb += ((int)cbCoefficients![coefficientIndex] - 128) * cbGrain[grainIndex]; + } + + if (applyCr) + { + weightedCr += ((int)crCoefficients![coefficientIndex] - 128) * crGrain[grainIndex]; + } + + coefficientIndex++; + } + + if (parameters.NumYPoints != 0) + { + int lumaRow = ((row - TemplatePadding) << subsamplingY) + TemplatePadding; + int lumaColumn = ((column - TemplatePadding) << subsamplingX) + TemplatePadding; + int averageLuma = 0; + for (int relativeRow = 0; relativeRow <= subsamplingY; relativeRow++) + { + for (int relativeColumn = 0; relativeColumn <= subsamplingX; relativeColumn++) + { + averageLuma += lumaGrain[((lumaRow + relativeRow) * lumaStride) + + lumaColumn + relativeColumn]; + } + } + + int averagingShift = subsamplingX + subsamplingY; + averageLuma = (averageLuma + ((1 << averagingShift) >> 1)) >> averagingShift; + if (applyCb) + { + weightedCb += ((int)cbCoefficients![coefficientIndex] - 128) * averageLuma; + } + + if (applyCr) + { + weightedCr += ((int)crCoefficients![coefficientIndex] - 128) * averageLuma; + } + } + + int currentIndex = (row * stride) + column; + if (applyCb) + { + cbGrain[currentIndex] = Av1Math.Clamp( + cbGrain[currentIndex] + ((weightedCb + roundingOffset) >> coefficientShift), + grainMinimum, + grainMaximum); + } + + if (applyCr) + { + crGrain[currentIndex] = Av1Math.Clamp( + crGrain[currentIndex] + ((weightedCr + roundingOffset) >> coefficientShift), + grainMinimum, + grainMaximum); + } + } + } + } + + /// + /// Fills one chroma template from the normative Gaussian sequence. + /// + /// The pseudo-random register advanced for each sample. + /// The destination grain template. + /// The template height. + /// The template width. + /// The template row stride. + /// The normative Gaussian sample sequence. + /// The bit-depth-dependent Gaussian scaling shift. + /// The Gaussian scaling rounding offset. + private static void FillGaussianGrain( + ref ushort randomRegister, + Span grain, + int height, + int width, + int stride, + ReadOnlySpan gaussian, + int gaussianShift, + int gaussianRounding) + { + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int randomIndex = GetRandomNumber(ref randomRegister, GaussianIndexBits); + grain[(row * stride) + column] = (gaussian[randomIndex] + gaussianRounding) >> gaussianShift; + } + } + } + + /// + /// Expands signaled control points into the 256-entry film-grain scaling lookup table. + /// + /// The strictly increasing input coordinates. + /// The scaling value at each input coordinate. + /// The number of populated control points. + /// The destination scaling lookup table. + private static void InitializeScalingFunction( + uint[]? pointValues, + uint[]? pointScalings, + int pointCount, + Span lookup) + { + if (pointCount == 0) + { + return; + } + + uint[] values = pointValues!; + uint[] scalings = pointScalings!; + lookup[..(int)values[0]].Fill((int)scalings[0]); + for (int point = 0; point < pointCount - 1; point++) + { + int deltaY = (int)scalings[point + 1] - (int)scalings[point]; + int deltaX = (int)values[point + 1] - (int)values[point]; + long delta = deltaY * ((65536 + (deltaX >> 1)) / deltaX); + for (int x = 0; x < deltaX; x++) + { + lookup[(int)values[point] + x] = (int)scalings[point] + + (int)(((x * delta) + 32768) >> 16); + } + } + + lookup[(int)values[pointCount - 1]..].Fill((int)scalings[pointCount - 1]); + } + + /// + /// Adds a selected luma/chroma grain region to one rectangular frame region. + /// + /// The native eight-bit or high-bit-depth sample type. + /// The frame grain parameters. + /// The luma scaling lookup table. + /// The first chroma scaling lookup table. + /// The second chroma scaling lookup table. + /// The destination luma region. + /// The destination first-chroma region. + /// The destination second-chroma region. + /// The destination luma row stride. + /// The destination chroma row stride. + /// The selected luma grain region. + /// The selected first-chroma grain region. + /// The selected second-chroma grain region. + /// The luma grain row stride. + /// The chroma grain row stride. + /// Half the destination luma height. + /// Half the destination luma width. + /// The decoded sample bit depth. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// Whether chroma processing is absent. + /// Whether chroma planes carry direct RGB components. + private static void AddNoiseToBlock( + ObuFilmGrainParameters parameters, + ReadOnlySpan scalingY, + ReadOnlySpan scalingCb, + ReadOnlySpan scalingCr, + Span luma, + Span cb, + Span cr, + int lumaStride, + int chromaStride, + ReadOnlySpan lumaGrain, + ReadOnlySpan cbGrain, + ReadOnlySpan crGrain, + int lumaGrainStride, + int chromaGrainStride, + int halfLumaHeight, + int halfLumaWidth, + int bitDepth, + int subsamplingX, + int subsamplingY, + bool isMonochrome, + bool isIdentityMatrix) + where TSample : unmanaged + { + int scalingShift = (int)parameters.GrainScalingMinus8 + 8; + int roundingOffset = 1 << (scalingShift - 1); + int depthScale = 1 << (bitDepth - 8); + int sampleMaximum = (256 * depthScale) - 1; + int lumaMinimum = 0; + int lumaMaximum = sampleMaximum; + int chromaMinimum = 0; + int chromaMaximum = sampleMaximum; + if (parameters.ClipToRestrictedRange) + { + lumaMinimum = RestrictedLumaMinimum * depthScale; + lumaMaximum = RestrictedLumaMaximum * depthScale; + chromaMinimum = (isIdentityMatrix ? RestrictedLumaMinimum : RestrictedChromaMinimum) * depthScale; + chromaMaximum = (isIdentityMatrix ? RestrictedLumaMaximum : RestrictedChromaMaximum) * depthScale; + } + + if (!isMonochrome) + { + int cbMultiplier = (int)parameters.CbMult - 128; + int cbLumaMultiplier = (int)parameters.CbLumaMult - 128; + int cbOffset = ((int)parameters.CbOffset * depthScale) - (256 * depthScale); + int crMultiplier = (int)parameters.CrMult - 128; + int crLumaMultiplier = (int)parameters.CrLumaMult - 128; + int crOffset = ((int)parameters.CrOffset * depthScale) - (256 * depthScale); + if (parameters.ChromaScalingFromLuma) + { + cbMultiplier = 0; + cbLumaMultiplier = 64; + cbOffset = 0; + crMultiplier = 0; + crLumaMultiplier = 64; + crOffset = 0; + } + + bool applyCb = parameters.NumCbPoints != 0 || parameters.ChromaScalingFromLuma; + bool applyCr = parameters.NumCrPoints != 0 || parameters.ChromaScalingFromLuma; + int chromaHeight = halfLumaHeight << (1 - subsamplingY); + int chromaWidth = halfLumaWidth << (1 - subsamplingX); + for (int row = 0; row < chromaHeight; row++) + { + for (int column = 0; column < chromaWidth; column++) + { + int lumaIndex = ((row << subsamplingY) * lumaStride) + (column << subsamplingX); + int averageLuma = GetSample(luma, lumaIndex); + if (subsamplingX != 0) + { + averageLuma = (averageLuma + GetSample(luma, lumaIndex + 1) + 1) >> 1; + } + + int chromaIndex = (row * chromaStride) + column; + int grainIndex = (row * chromaGrainStride) + column; + if (applyCb) + { + int cbSample = GetSample(cb, chromaIndex); + int scalingIndex = Av1Math.Clamp( + (((averageLuma * cbLumaMultiplier) + (cbMultiplier * cbSample)) >> 6) + cbOffset, + 0, + sampleMaximum); + + int grainScale = ScaleLookup(scalingCb, scalingIndex, bitDepth); + SetSample( + cb, + chromaIndex, + Av1Math.Clamp( + cbSample + (((grainScale * cbGrain[grainIndex]) + roundingOffset) >> scalingShift), + chromaMinimum, + chromaMaximum)); + } + + if (applyCr) + { + int crSample = GetSample(cr, chromaIndex); + int scalingIndex = Av1Math.Clamp( + (((averageLuma * crLumaMultiplier) + (crMultiplier * crSample)) >> 6) + crOffset, + 0, + sampleMaximum); + + int grainScale = ScaleLookup(scalingCr, scalingIndex, bitDepth); + SetSample( + cr, + chromaIndex, + Av1Math.Clamp( + crSample + (((grainScale * crGrain[grainIndex]) + roundingOffset) >> scalingShift), + chromaMinimum, + chromaMaximum)); + } + } + } + } + + if (parameters.NumYPoints != 0) + { + int lumaHeight = halfLumaHeight << 1; + int lumaWidth = halfLumaWidth << 1; + for (int row = 0; row < lumaHeight; row++) + { + for (int column = 0; column < lumaWidth; column++) + { + int sampleIndex = (row * lumaStride) + column; + int sample = GetSample(luma, sampleIndex); + int grainScale = ScaleLookup(scalingY, sample, bitDepth); + int grainIndex = (row * lumaGrainStride) + column; + SetSample( + luma, + sampleIndex, + Av1Math.Clamp( + sample + (((grainScale * lumaGrain[grainIndex]) + roundingOffset) >> scalingShift), + lumaMinimum, + lumaMaximum)); + } + } + } + } + + /// + /// Reads a bit-depth scaling value, interpolating between eight-bit entries when required. + /// + /// The 256-entry scaling lookup table. + /// The sample-scale lookup coordinate. + /// The decoded sample bit depth. + /// The interpolated grain scaling value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ScaleLookup(ReadOnlySpan lookup, int index, int bitDepth) + { + int depthShift = bitDepth - 8; + int lookupIndex = index >> depthShift; + if (depthShift == 0 || lookupIndex == 255) + { + return lookup[lookupIndex]; + } + + int fraction = index & ((1 << depthShift) - 1); + return lookup[lookupIndex] + + ((((lookup[lookupIndex + 1] - lookup[lookupIndex]) * fraction) + (1 << (depthShift - 1))) >> depthShift); + } + + /// + /// Blends the two grain columns on a vertical block boundary. + /// + /// The saved grain columns from the block on the left. + /// The saved-column row stride. + /// The grain columns selected for the block on the right. + /// The right-block row stride. + /// The overlap destination. + /// The destination row stride. + /// The one- or two-sample overlap width. + /// The overlap height. + /// The minimum grain value. + /// The maximum grain value. + private static void VerticalOverlap( + ReadOnlySpan left, + int leftStride, + ReadOnlySpan right, + int rightStride, + Span destination, + int destinationStride, + int width, + int height, + int minimum, + int maximum) + { + for (int row = 0; row < height; row++) + { + int leftOffset = row * leftStride; + int rightOffset = row * rightStride; + int destinationOffset = row * destinationStride; + if (width == 1) + { + destination[destinationOffset] = Av1Math.Clamp( + ((left[leftOffset] * 23) + (right[rightOffset] * 22) + 16) >> 5, + minimum, + maximum); + } + else + { + destination[destinationOffset] = Av1Math.Clamp( + ((left[leftOffset] * 27) + (right[rightOffset] * 17) + 16) >> 5, + minimum, + maximum); + + destination[destinationOffset + 1] = Av1Math.Clamp( + ((left[leftOffset + 1] * 17) + (right[rightOffset + 1] * 27) + 16) >> 5, + minimum, + maximum); + } + } + } + + /// + /// Blends the one or two grain rows on a horizontal block boundary. + /// + /// The saved grain rows from the block above. + /// The saved-row stride. + /// The grain rows selected for the block below. + /// The lower-block row stride. + /// The overlap destination. + /// The destination row stride. + /// The overlap width. + /// The one- or two-sample overlap height. + /// The minimum grain value. + /// The maximum grain value. + private static void HorizontalOverlap( + ReadOnlySpan top, + int topStride, + ReadOnlySpan bottom, + int bottomStride, + Span destination, + int destinationStride, + int width, + int height, + int minimum, + int maximum) + { + for (int column = 0; column < width; column++) + { + if (height == 1) + { + destination[column] = Av1Math.Clamp( + ((top[column] * 23) + (bottom[column] * 22) + 16) >> 5, + minimum, + maximum); + } + else + { + destination[column] = Av1Math.Clamp( + ((top[column] * 27) + (bottom[column] * 17) + 16) >> 5, + minimum, + maximum); + + destination[destinationStride + column] = Av1Math.Clamp( + ((top[topStride + column] * 17) + (bottom[bottomStride + column] * 27) + 16) >> 5, + minimum, + maximum); + } + } + } + + /// + /// Copies a rectangular grain region while preserving independent source and destination strides. + /// + /// The source region. + /// The source row stride. + /// The destination region. + /// The destination row stride. + /// The number of values copied per row. + /// The number of rows copied. + private static void CopyArea( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + { + for (int row = 0; row < height; row++) + { + source.Slice(row * sourceStride, width) + .CopyTo(destination.Slice(row * destinationStride, width)); + } + } + + /// + /// Initializes the film-grain linear-feedback shift register for one luma block row. + /// + /// The register to initialize. + /// The zero-based luma row represented by the block row. + /// The frame grain seed. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InitializeRandomGenerator(ref ushort randomRegister, int lumaLine, ushort seed) + { + randomRegister = seed; + int lumaBlock = lumaLine >> 5; + randomRegister ^= (ushort)((((lumaBlock * 37) + 178) & 255) << 8); + randomRegister ^= (ushort)(((lumaBlock * 173) + 105) & 255); + } + + /// + /// Advances the film-grain linear-feedback shift register and returns its high-order bits. + /// + /// The register to advance. + /// The number of result bits. + /// A value in the range zero through 2^bitCount - 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetRandomNumber(ref ushort randomRegister, int bitCount) + { + int feedback = (randomRegister ^ (randomRegister >> 1) ^ (randomRegister >> 3) ^ + (randomRegister >> 12)) & 1; + + randomRegister = (ushort)((randomRegister >> 1) | (feedback << 15)); + return (randomRegister >> (16 - bitCount)) & ((1 << bitCount) - 1); + } + + /// + /// Reads an eight-bit or high-bit-depth sample without a format-conversion buffer. + /// + /// The native sample type. + /// The sample span. + /// The sample index. + /// The unsigned sample value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetSample(Span samples, int index) + where TSample : unmanaged + { + ref TSample sample = ref samples[index]; + return typeof(TSample) == typeof(byte) + ? Unsafe.As(ref sample) + : Unsafe.As(ref sample); + } + + /// + /// Writes an eight-bit or high-bit-depth sample without a format-conversion buffer. + /// + /// The native sample type. + /// The sample span. + /// The sample index. + /// The already clipped unsigned value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetSample(Span samples, int index, int value) + where TSample : unmanaged + { + if (typeof(TSample) == typeof(byte)) + { + byte byteValue = (byte)value; + samples[index] = Unsafe.As(ref byteValue); + } + else + { + ushort ushortValue = (ushort)value; + samples[index] = Unsafe.As(ref ushortValue); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainGaussianSequence.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainGaussianSequence.cs new file mode 100644 index 0000000000..a8d1df4854 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainGaussianSequence.cs @@ -0,0 +1,150 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.FilmGrain; + +/// +/// Provides the normative 2,048-sample Gaussian sequence used by AV1 film-grain synthesis. +/// +internal static class Av1FilmGrainGaussianSequence +{ + /// + /// The signed 12-bit Gaussian samples indexed by the film-grain pseudo-random generator. + /// + private static readonly short[] Values = + [ + 56, 568, -180, 172, 124, -84, 172, -64, -900, 24, 820, 224, 1248, 996, 272, -8, + -916, -388, -732, -104, -188, 800, 112, -652, -320, -376, 140, -252, 492, -168, 44, -788, + 588, -584, 500, -228, 12, 680, 272, -476, 972, -100, 652, 368, 432, -196, -720, -192, + 1000, -332, 652, -136, -552, -604, -4, 192, -220, -136, 1000, -52, 372, -96, -624, 124, + -24, 396, 540, -12, -104, 640, 464, 244, -208, -84, 368, -528, -740, 248, -968, -848, + 608, 376, -60, -292, -40, -156, 252, -292, 248, 224, -280, 400, -244, 244, -60, 76, + -80, 212, 532, 340, 128, -36, 824, -352, -60, -264, -96, -612, 416, -704, 220, -204, + 640, -160, 1220, -408, 900, 336, 20, -336, -96, -792, 304, 48, -28, -1232, -1172, -448, + 104, -292, -520, 244, 60, -948, 0, -708, 268, 108, 356, -548, 488, -344, -136, 488, + -196, -224, 656, -236, -1128, 60, 4, 140, 276, -676, -376, 168, -108, 464, 8, 564, + 64, 240, 308, -300, -400, -456, -136, 56, 120, -408, -116, 436, 504, -232, 328, 844, + -164, -84, 784, -168, 232, -224, 348, -376, 128, 568, 96, -1244, -288, 276, 848, 832, + -360, 656, 464, -384, -332, -356, 728, -388, 160, -192, 468, 296, 224, 140, -776, -100, + 280, 4, 196, 44, -36, -648, 932, 16, 1428, 28, 528, 808, 772, 20, 268, 88, + -332, -284, 124, -384, -448, 208, -228, -1044, -328, 660, 380, -148, -300, 588, 240, 540, + 28, 136, -88, -436, 256, 296, -1000, 1400, 0, -48, 1056, -136, 264, -528, -1108, 632, + -484, -592, -344, 796, 124, -668, -768, 388, 1296, -232, -188, -200, -288, -4, 308, 100, + -168, 256, -500, 204, -508, 648, -136, 372, -272, -120, -1004, -552, -548, -384, 548, -296, + 428, -108, -8, -912, -324, -224, -88, -112, -220, -100, 996, -796, 548, 360, -216, 180, + 428, -200, -212, 148, 96, 148, 284, 216, -412, -320, 120, -300, -384, -604, -572, -332, + -8, -180, -176, 696, 116, -88, 628, 76, 44, -516, 240, -208, -40, 100, -592, 344, + -308, -452, -228, 20, 916, -1752, -136, -340, -804, 140, 40, 512, 340, 248, 184, -492, + 896, -156, 932, -628, 328, -688, -448, -616, -752, -100, 560, -1020, 180, -800, -64, 76, + 576, 1068, 396, 660, 552, -108, -28, 320, -628, 312, -92, -92, -472, 268, 16, 560, + 516, -672, -52, 492, -100, 260, 384, 284, 292, 304, -148, 88, -152, 1012, 1064, -228, + 164, -376, -684, 592, -392, 156, 196, -524, -64, -884, 160, -176, 636, 648, 404, -396, + -436, 864, 424, -728, 988, -604, 904, -592, 296, -224, 536, -176, -920, 436, -48, 1176, + -884, 416, -776, -824, -884, 524, -548, -564, -68, -164, -96, 692, 364, -692, -1012, -68, + 260, -480, 876, -1116, 452, -332, -352, 892, -1088, 1220, -676, 12, -292, 244, 496, 372, + -32, 280, 200, 112, -440, -96, 24, -644, -184, 56, -432, 224, -980, 272, -260, 144, + -436, 420, 356, 364, -528, 76, 172, -744, -368, 404, -752, -416, 684, -688, 72, 540, + 416, 92, 444, 480, -72, -1416, 164, -1172, -68, 24, 424, 264, 1040, 128, -912, -524, + -356, 64, 876, -12, 4, -88, 532, 272, -524, 320, 276, -508, 940, 24, -400, -120, + 756, 60, 236, -412, 100, 376, -484, 400, -100, -740, -108, -260, 328, -268, 224, -200, + -416, 184, -604, -564, -20, 296, 60, 892, -888, 60, 164, 68, -760, 216, -296, 904, + -336, -28, 404, -356, -568, -208, -1480, -512, 296, 328, -360, -164, -1560, -776, 1156, -428, + 164, -504, -112, 120, -216, -148, -264, 308, 32, 64, -72, 72, 116, 176, -64, -272, + 460, -536, -784, -280, 348, 108, -752, -132, 524, -540, -776, 116, -296, -1196, -288, -560, + 1040, -472, 116, -848, -1116, 116, 636, 696, 284, -176, 1016, 204, -864, -648, -248, 356, + 972, -584, -204, 264, 880, 528, -24, -184, 116, 448, -144, 828, 524, 212, -212, 52, + 12, 200, 268, -488, -404, -880, 824, -672, -40, 908, -248, 500, 716, -576, 492, -576, + 16, 720, -108, 384, 124, 344, 280, 576, -500, 252, 104, -308, 196, -188, -8, 1268, + 296, 1032, -1196, 436, 316, 372, -432, -200, -660, 704, -224, 596, -132, 268, 32, -452, + 884, 104, -1008, 424, -1348, -280, 4, -1168, 368, 476, 696, 300, -8, 24, 180, -592, + -196, 388, 304, 500, 724, -160, 244, -84, 272, -256, -420, 320, 208, -144, -156, 156, + 364, 452, 28, 540, 316, 220, -644, -248, 464, 72, 360, 32, -388, 496, -680, -48, + 208, -116, -408, 60, -604, -392, 548, -840, 784, -460, 656, -544, -388, -264, 908, -800, + -628, -612, -568, 572, -220, 164, 288, -16, -308, 308, -112, -636, -760, 280, -668, 432, + 364, 240, -196, 604, 340, 384, 196, 592, -44, -500, 432, -580, -132, 636, -76, 392, + 4, -412, 540, 508, 328, -356, -36, 16, -220, -64, -248, -60, 24, -192, 368, 1040, + 92, -24, -1044, -32, 40, 104, 148, 192, -136, -520, 56, -816, -224, 732, 392, 356, + 212, -80, -424, -1008, -324, 588, -1496, 576, 460, -816, -848, 56, -580, -92, -1372, -112, + -496, 200, 364, 52, -140, 48, -48, -60, 84, 72, 40, 132, -356, -268, -104, -284, + -404, 732, -520, 164, -304, -540, 120, 328, -76, -460, 756, 388, 588, 236, -436, -72, + -176, -404, -316, -148, 716, -604, 404, -72, -88, -888, -68, 944, 88, -220, -344, 960, + 472, 460, -232, 704, 120, 832, -228, 692, -508, 132, -476, 844, -748, -364, -44, 1116, + -1104, -1056, 76, 428, 552, -692, 60, 356, 96, -384, -188, -612, -576, 736, 508, 892, + 352, -1132, 504, -24, -352, 324, 332, -600, -312, 292, 508, -144, -8, 484, 48, 284, + -260, -240, 256, -100, -292, -204, -44, 472, -204, 908, -188, -1000, -256, 92, 1164, -392, + 564, 356, 652, -28, -884, 256, 484, -192, 760, -176, 376, -524, -452, -436, 860, -736, + 212, 124, 504, -476, 468, 76, -472, 552, -692, -944, -620, 740, -240, 400, 132, 20, + 192, -196, 264, -668, -1012, -60, 296, -316, -828, 76, -156, 284, -768, -448, -832, 148, + 248, 652, 616, 1236, 288, -328, -400, -124, 588, 220, 520, -696, 1032, 768, -740, -92, + -272, 296, 448, -464, 412, -200, 392, 440, -200, 264, -152, -260, 320, 1032, 216, 320, + -8, -64, 156, -1016, 1084, 1172, 536, 484, -432, 132, 372, -52, -256, 84, 116, -352, + 48, 116, 304, -384, 412, 924, -300, 528, 628, 180, 648, 44, -980, -220, 1320, 48, + 332, 748, 524, -268, -720, 540, -276, 564, -344, -208, -196, 436, 896, 88, -392, 132, + 80, -964, -288, 568, 56, -48, -456, 888, 8, 552, -156, -292, 948, 288, 128, -716, + -292, 1192, -152, 876, 352, -600, -260, -812, -468, -28, -120, -32, -44, 1284, 496, 192, + 464, 312, -76, -516, -380, -456, -1012, -48, 308, -156, 36, 492, -156, -808, 188, 1652, + 68, -120, -116, 316, 160, -140, 352, 808, -416, 592, 316, -480, 56, 528, -204, -568, + 372, -232, 752, -344, 744, -4, 324, -416, -600, 768, 268, -248, -88, -132, -420, -432, + 80, -288, 404, -316, -1216, -588, 520, -108, 92, -320, 368, -480, -216, -92, 1688, -300, + 180, 1020, -176, 820, -68, -228, -260, 436, -904, 20, 40, -508, 440, -736, 312, 332, + 204, 760, -372, 728, 96, -20, -632, -520, -560, 336, 1076, -64, -532, 776, 584, 192, + 396, -728, -520, 276, -188, 80, -52, -612, -252, -48, 648, 212, -688, 228, -52, -260, + 428, -412, -272, -404, 180, 816, -796, 48, 152, 484, -88, -216, 988, 696, 188, -528, + 648, -116, -180, 316, 476, 12, -564, 96, 476, -252, -364, -376, -392, 556, -256, -576, + 260, -352, 120, -16, -136, -260, -492, 72, 556, 660, 580, 616, 772, 436, 424, -32, + -324, -1268, 416, -324, -80, 920, 160, 228, 724, 32, -516, 64, 384, 68, -128, 136, + 240, 248, -204, -68, 252, -932, -120, -480, -628, -84, 192, 852, -404, -288, -132, 204, + 100, 168, -68, -196, -868, 460, 1080, 380, -80, 244, 0, 484, -888, 64, 184, 352, + 600, 460, 164, 604, -196, 320, -64, 588, -184, 228, 12, 372, 48, -848, -344, 224, + 208, -200, 484, 128, -20, 272, -468, -840, 384, 256, -720, -520, -464, -580, 112, -120, + 644, -356, -208, -608, -528, 704, 560, -424, 392, 828, 40, 84, 200, -152, 0, -144, + 584, 280, -120, 80, -556, -972, -196, -472, 724, 80, 168, -32, 88, 160, -688, 0, + 160, 356, 372, -776, 740, -128, 676, -248, -480, 4, -364, 96, 544, 232, -1032, 956, + 236, 356, 20, -40, 300, 24, -676, -596, 132, 1120, -104, 532, -1096, 568, 648, 444, + 508, 380, 188, -376, -604, 1488, 424, 24, 756, -220, -192, 716, 120, 920, 688, 168, + 44, -460, 568, 284, 1144, 1160, 600, 424, 888, 656, -356, -320, 220, 316, -176, -724, + -188, -816, -628, -348, -228, -380, 1012, -452, -660, 736, 928, 404, -696, -72, -268, -892, + 128, 184, -344, -780, 360, 336, 400, 344, 428, 548, -112, 136, -228, -216, -820, -516, + 340, 92, -136, 116, -300, 376, -244, 100, -316, -520, -284, -12, 824, 164, -548, -180, + -128, 116, -924, -828, 268, -368, -580, 620, 192, 160, 0, -1676, 1068, 424, -56, -360, + 468, -156, 720, 288, -528, 556, -364, 548, -148, 504, 316, 152, -648, -620, -684, -24, + -376, -384, -108, -920, -1032, 768, 180, -264, -508, -1268, -260, -60, 300, -240, 988, 724, + -376, -576, -212, -736, 556, 192, 1092, -620, -880, 376, -56, -4, -216, -32, 836, 268, + 396, 1332, 864, -600, 100, 56, -412, -92, 356, 180, 884, -468, -436, 292, -388, -804, + -704, -840, 368, -348, 140, -724, 1536, 940, 372, 112, -372, 436, -480, 1136, 296, -32, + -228, 132, -48, -220, 868, -1016, -60, -1044, -464, 328, 916, 244, 12, -736, -296, 360, + 468, -376, -108, -92, 788, 368, -56, 544, 400, -672, -420, 728, 16, 320, 44, -284, + -380, -796, 488, 132, 204, -596, -372, 88, -152, -908, -636, -572, -624, -116, -692, -200, + -56, 276, -88, 484, -324, 948, 864, 1000, -456, -184, -276, 292, -296, 156, 676, 320, + 160, 908, -84, -1236, -288, -116, 260, -372, -644, 732, -756, -96, 84, 344, -520, 348, + -688, 240, -84, 216, -1044, -136, -676, -396, -1500, 960, -40, 176, 168, 1516, 420, -504, + -344, -364, -360, 1216, -940, -380, -212, 252, -660, -708, 484, -444, -152, 928, -120, 1112, + 476, -260, 560, -148, -344, 108, -196, 228, -288, 504, 560, -328, -88, 288, -1008, 460, + -228, 468, -836, -196, 76, 388, 232, 412, -1168, -716, -644, 756, -172, -356, -504, 116, + 432, 528, 48, 476, -168, -608, 448, 160, -532, -272, 28, -676, -12, 828, 980, 456, + 520, 104, -104, 256, -344, -4, -28, -368, -52, -524, -572, -556, -200, 768, 1124, -208, + -512, 176, 232, 248, -148, -888, 604, -600, -304, 804, -156, -212, 488, -192, -804, -256, + 368, -360, -916, -328, 228, -240, -448, -472, 856, -556, -364, 572, -12, -156, -368, -340, + 432, 252, -752, -152, 288, 268, -580, -848, -592, 108, -76, 244, 312, -716, 592, -80, + 436, 360, 4, -248, 160, 516, 584, 732, 44, -468, -280, -292, -156, -588, 28, 308, + 912, 24, 124, 156, 180, -252, 944, -924, -772, -520, -428, -624, 300, -212, -1144, 32, + -724, 800, -1128, -212, -1288, -848, 180, -416, 440, 192, -576, -792, -76, -1080, 80, -532, + -352, -132, 380, -820, 148, 1112, 128, 164, 456, 700, -924, 144, -668, -384, 648, -832, + 508, 552, -52, -100, -656, 208, -568, 748, -88, 680, 232, 300, 192, -408, -1012, -152, + -252, -268, 272, -876, -664, -648, -332, -136, 16, 12, 1152, -28, 332, -536, 320, -672, + -460, -316, 532, -260, 228, -40, 1052, -816, 180, 88, -496, -556, -672, -368, 428, 92, + 356, 404, -408, 252, 196, -176, -556, 792, 268, 32, 372, 40, 96, -332, 328, 120, + 372, -900, -40, 472, -264, -592, 952, 128, 656, 112, 664, -232, 420, 4, -344, -464, + 556, 244, -416, -32, 252, 0, -412, 188, -696, 508, -476, 324, -1096, 656, -312, 560, + 264, -136, 304, 160, -64, -580, 248, 336, -720, 560, -348, -288, -276, -196, -500, 852, + -544, -236, -1128, -992, -776, 116, 56, 52, 860, 884, 212, -12, 168, 1020, 512, -552, + 924, -148, 716, 188, 164, -340, -520, -184, 880, -152, -680, -208, -1156, -300, -528, -472, + 364, 100, -744, -1056, -32, 540, 280, 144, -676, -32, -232, -280, -224, 96, 568, -76, + 172, 148, 148, 104, 32, -296, -32, 788, -80, 32, -16, 280, 288, 944, 428, -484, + ]; + + /// + /// Gets the Gaussian samples in their normative index order. + /// + public static ReadOnlySpan Samples => Values; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs index cd6321bb36..8d636d981e 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs @@ -361,8 +361,6 @@ internal class Av1PredictionDecoder Av1BitDepth bitDepth) where T : unmanaged, IBinaryInteger { - // TODO:are_parameters_computed variable for CFL so that cal part for V plane we can skip, - // once we compute for U plane, this parameter is block level parameter. ObuColorConfig cc = this.sequenceHeader.ColorConfig; int subX = plane != Av1Plane.Y ? cc.SubSamplingX ? 1 : 0 : 0; int subY = plane != Av1Plane.Y ? cc.SubSamplingY ? 1 : 0 : 0;