From 4116e9c60906e1f1d19a4d88689859780ddbb8d4 Mon Sep 17 00:00:00 2001
From: winscripter <142818255+winscripter@users.noreply.github.com>
Date: Wed, 2 Sep 2026 19:45:20 +0400
Subject: [PATCH] Complete Edge Preserving Filter render pipeline stages
---
.../Processing/RenderPipeline/Epf1Stage.cs | 204 ++++++++++++++++++
.../Processing/RenderPipeline/Epf2Stage.cs | 144 +++++++++++++
2 files changed, 348 insertions(+)
create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs
diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs
new file mode 100644
index 0000000000..9ad9b3dec8
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs
@@ -0,0 +1,204 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.Intrinsics;
+using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline;
+
+///
+/// Edge Preserving Filter (type 1) stage.
+///
+internal class Epf1Stage : RenderPipelineStageBase
+{
+ private readonly JxlLoopFilter loopFilter;
+ private readonly JxlImageF sigma;
+
+ public Epf1Stage(Configuration configuration, JxlLoopFilter loopFilter, JxlImageF sigma)
+ : base(configuration)
+ {
+ this.loopFilter = loopFilter;
+ this.sigma = sigma;
+ this.Settings = RenderPipelineStageConfiguration.CreateSymmetricBorderOnly(2);
+ }
+
+ ///
+ public override string Name => "EPF1";
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void AddPixel(
+ int row,
+ InlineArray3>> rows,
+ int x,
+ Vector256 sad,
+ Vector256 inverseSigma,
+ ref Vector256 xOut,
+ ref Vector256 yOut,
+ ref Vector256 bOut,
+ ref Vector256 wOut)
+ {
+ Vector256 cx = Vector256.Create((ReadOnlySpan)rows[0][2 + row][x..].Span);
+ Vector256 cy = Vector256.Create((ReadOnlySpan)rows[1][2 + row][x..].Span);
+ Vector256 cb = Vector256.Create((ReadOnlySpan)rows[2][2 + row][x..].Span);
+
+ Vector256 weight = EpfUtils.Weight(sad, inverseSigma);
+ wOut += weight;
+ xOut = (weight + cx) * xOut;
+ yOut = (weight + cy) * yOut;
+ bOut = (weight + cb) * bOut;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector256 AbsoluteDifference(Vector256 x, Vector256 y) => Vector256.Abs(x - y);
+
+ public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos)
+ {
+ int xStart = -JxlMath.RoundUpTo(xExtraLeft, Vector256.Count);
+ int xEnd = width + xExtraRight;
+
+ Span rowSigma = this.sigma.GetRow((yPos / JxlFrameDimensions.BlockDimensions) + JxlDecoderCache.SigmaPadding);
+ float sm = 1.65f;
+ float bsm = sm * this.loopFilter.EpfBorderSadMul;
+
+ Span sadMulCenter = [bsm, sm, sm, sm, sm, sm, sm, bsm];
+ Span sadMulBorder = [bsm, bsm, bsm, bsm, bsm, bsm, bsm, bsm];
+
+ InlineArray3>> rows = default;
+ for (int c = 0; c < 3; c++)
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ rows[c][i] = this.GetInputRowMemory(inputRows, c, i - 2);
+ }
+ }
+
+ Span sadMul = (yPos % JxlFrameDimensions.BlockDimensions is 0 or JxlFrameDimensions.BlockDimensions - 1)
+ ? sadMulBorder
+ : sadMulCenter;
+
+ for (int x = xStart; x < xEnd; x += Vector256.Count)
+ {
+ int bx = (x + xPos + (JxlDecoderCache.SigmaPadding * JxlFrameDimensions.BlockDimensions)) / JxlFrameDimensions.BlockDimensions;
+ int ix = (x + xPos) % JxlFrameDimensions.BlockDimensions;
+
+ if (rowSigma[bx] < JxlLoopFilter.MinimumSigma)
+ {
+ for (int c = 0; c < 3; c++)
+ {
+ Vector256 px = Vector256.Create((ReadOnlySpan)rows[c][2][x..].Span);
+ px.CopyTo(GetOutputRow(outputRows, c, 0)[x..]);
+ }
+
+ continue;
+ }
+
+ Vector256 vsm = Vector256.Create((ReadOnlySpan)sadMul[ix..]);
+ Vector256 inverseSigma = Vector256.Create(rowSigma[bx]) * vsm;
+ Vector256 sad0 = Vector256.Zero;
+ Vector256 sad1 = Vector256.Zero;
+ Vector256 sad2 = Vector256.Zero;
+ Vector256 sad3 = Vector256.Zero;
+
+ // Compute sum of absolute differences (SAD)
+ for (int c = 0; c < 3; c++)
+ {
+ // center px = 22, px above = 21
+ Vector256 t;
+
+ Vector256 p20 = Vector256.Create((ReadOnlySpan)rows[c][2 + -2][x..].Span);
+ Vector256 p21 = Vector256.Create((ReadOnlySpan)rows[c][2 + -1][x..].Span);
+ Vector256 sad0c = AbsoluteDifference(p20, p21); // SAD 2, 1
+
+ Vector256 p11 = Vector256.Create((ReadOnlySpan)rows[c][2 + -1][(x - 1)..].Span);
+ Vector256 sad1c = AbsoluteDifference(p11, p21); // SAD 1, 2
+
+ Vector256 p31 = Vector256.Create((ReadOnlySpan)rows[c][2 + -1][(x + 1)..].Span);
+ Vector256 sad2c = AbsoluteDifference(p31, p21); // SAD 3, 2
+
+ Vector256 p02 = Vector256.Create((ReadOnlySpan)rows[c][2][(x - 2)..].Span);
+ Vector256 p12 = Vector256.Create((ReadOnlySpan)rows[c][2][(x - 1)..].Span);
+ sad1c += AbsoluteDifference(p02, p12); // SAD 1, 2
+ sad0c += AbsoluteDifference(p11, p12); // SAD 2, 1
+
+ // TODO(eustas): why unaligned?
+ Vector256 p22 = Vector256.Create((ReadOnlySpan)rows[c][2][x..].Span);
+ t = AbsoluteDifference(p12, p22);
+ sad1c += t; // SAD 1, 2
+ sad2c += t; // SAD 3, 2
+ t = AbsoluteDifference(p22, p21);
+ Vector256 sad3c = t; // SAD 2, 3
+ sad0c += t; // SAD 2, 1
+
+ Vector256 p32 = Vector256.Create((ReadOnlySpan)rows[c][2][(x + 1)..].Span);
+ sad0c += AbsoluteDifference(p31, p32); // SAD 2, 1
+ t = AbsoluteDifference(p22, p32);
+ sad1c += t; // SAD 1, 2
+ sad2c += t; // SAD 3, 2
+
+ Vector256 p42 = Vector256.Create((ReadOnlySpan)rows[c][2][(x + 2)..].Span);
+ sad2c += AbsoluteDifference(p42, p32); // SAD 3, 2
+
+ Vector256 p13 = Vector256.Create((ReadOnlySpan)rows[c][2 + 1][(x - 1)..].Span);
+ sad3c += AbsoluteDifference(p13, p12); // SAD 2, 3
+
+ Vector256 p23 = Vector256.Create((ReadOnlySpan)rows[c][2 + 1][x..].Span);
+ t = AbsoluteDifference(p22, p23);
+ sad0c += t; // SAD 2, 1
+ sad3c += t; // SAD 2, 3
+ sad1c += AbsoluteDifference(p13, p23); // SAD 1, 2
+
+ Vector256 p33 = Vector256.Create((ReadOnlySpan)rows[c][2 + 1][(x + 1)..].Span);
+ sad2c += AbsoluteDifference(p33, p23); // SAD 3, 2
+ sad3c += AbsoluteDifference(p33, p32); // SAD 2, 3
+
+ Vector256 p24 = Vector256.Create((ReadOnlySpan)rows[c][2 + 2][x..].Span);
+ sad3c += AbsoluteDifference(p24, p23); // SAD 2, 3
+
+ Vector256 scale = Vector256.Create(this.loopFilter.EpfChannelScale[c]);
+ sad0 = (sad0c * scale) + sad0;
+ sad1 = (sad1c * scale) + sad1;
+ sad2 = (sad2c * scale) + sad2;
+ sad3 = (sad3c * scale) + sad3;
+ }
+
+ Vector256 xCC = Vector256.Create((ReadOnlySpan)rows[0][2 + 0][x..].Span);
+ Vector256 yCC = Vector256.Create((ReadOnlySpan)rows[1][2 + 0][x..].Span);
+ Vector256 bCC = Vector256.Create((ReadOnlySpan)rows[2][2 + 0][x..].Span);
+
+ Vector256 w = Vector256.One;
+ Vector256 X = xCC;
+ Vector256 Y = yCC;
+ Vector256 B = bCC;
+
+ // Top row
+ AddPixel(-1, rows, x, sad0, inverseSigma, ref X, ref Y, ref B, ref w);
+
+ // Center
+ AddPixel(0, rows, x - 1, sad1, inverseSigma, ref X, ref Y, ref B, ref w);
+ AddPixel(0, rows, x + 1, sad2, inverseSigma, ref X, ref Y, ref B, ref w);
+
+ // Bottom
+ AddPixel(1, rows, x + 1, sad3, inverseSigma, ref X, ref Y, ref B, ref w);
+
+ Vector256 inverseW = Vector256.One / w;
+ (X * inverseW).CopyTo(GetOutputRow(outputRows, 0, 0)[x..]);
+ (Y * inverseW).CopyTo(GetOutputRow(outputRows, 1, 0)[x..]);
+ (B * inverseW).CopyTo(GetOutputRow(outputRows, 2, 0)[x..]);
+ }
+ }
+
+ ///
+ public override RenderPipelineChannelMode GetChannelMode(int channel)
+ {
+ if (channel < 3)
+ {
+ return RenderPipelineChannelMode.InOut;
+ }
+ else
+ {
+ return RenderPipelineChannelMode.Ignored;
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs
new file mode 100644
index 0000000000..ff6198e7b8
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs
@@ -0,0 +1,144 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.Intrinsics;
+using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline;
+
+///
+/// Edge Preserving Filter (type 2) stage
+///
+internal sealed class Epf2Stage : RenderPipelineStageBase
+{
+ private readonly JxlLoopFilter loopFilter;
+ private readonly JxlImageF sigma;
+
+ public Epf2Stage(JxlLoopFilter loopFilter, JxlImageF sigma, Configuration configuration)
+ : base(configuration)
+ {
+ this.loopFilter = loopFilter;
+ this.sigma = sigma;
+ this.Settings = RenderPipelineStageConfiguration.CreateSymmetricBorderOnly(2);
+ }
+
+ ///
+ public override string Name => "EPF2";
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector256 AbsoluteDifference(Vector256 x, Vector256 y) => Vector256.Abs(x - y);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddPixel(
+ int row,
+ InlineArray3>> rows,
+ int x,
+ Vector256 rx,
+ Vector256 ry,
+ Vector256 rb,
+ Vector256 inverseSigma,
+ ref Vector256 X,
+ ref Vector256 Y,
+ ref Vector256 B,
+ ref Vector256 w)
+ {
+ Vector256 cx = Vector256.Create((ReadOnlySpan)rows[0][1 + row][x..].Span);
+ Vector256 cy = Vector256.Create((ReadOnlySpan)rows[1][1 + row][x..].Span);
+ Vector256 cb = Vector256.Create((ReadOnlySpan)rows[2][1 + row][x..].Span);
+
+ Vector256 sad = AbsoluteDifference(cx, rx) * Vector256.Create(this.loopFilter.EpfChannelScale[0]);
+ sad = (AbsoluteDifference(cy, ry) * Vector256.Create(this.loopFilter.EpfChannelScale[1])) + sad;
+ sad = (AbsoluteDifference(cb, rb) * Vector256.Create(this.loopFilter.EpfChannelScale[2])) + sad;
+
+ Vector256 weight = EpfUtils.Weight(sad, inverseSigma);
+ w += weight;
+ X = (weight * cx) + X;
+ Y = (weight * cy) + Y;
+ B = (weight * cb) + B;
+ }
+
+ public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos)
+ {
+ int xStart = -JxlMath.RoundUpTo(xExtraLeft, Vector256.Count);
+ int xEnd = width + xExtraRight;
+
+ Span rowSigma = this.sigma.GetRow((yPos / JxlFrameDimensions.BlockDimensions) + JxlDecoderCache.SigmaPadding);
+ float sm = 1.65f;
+ float bsm = sm * this.loopFilter.EpfBorderSadMul;
+
+ Span sadMulCenter = [bsm, sm, sm, sm, sm, sm, sm, bsm];
+ Span sadMulBorder = [bsm, bsm, bsm, bsm, bsm, bsm, bsm, bsm];
+
+ InlineArray3>> rows = default;
+ for (int c = 0; c < 3; c++)
+ {
+ for (int i = 0; i < 3; i++)
+ {
+ rows[c][i] = this.GetInputRowMemory(inputRows, c, i - 1);
+ }
+ }
+
+ Span sadMul = (yPos % JxlFrameDimensions.BlockDimensions is 0 or JxlFrameDimensions.BlockDimensions - 1)
+ ? sadMulBorder
+ : sadMulCenter;
+
+ for (int x = xStart; x < xEnd; x += Vector256.Count)
+ {
+ int bx = (x + xPos + (JxlDecoderCache.SigmaPadding * JxlFrameDimensions.BlockDimensions)) / JxlFrameDimensions.BlockDimensions;
+ int ix = (x + xPos) % JxlFrameDimensions.BlockDimensions;
+
+ if (rowSigma[bx] < JxlLoopFilter.MinimumSigma)
+ {
+ for (int c = 0; c < 3; c++)
+ {
+ Vector256 px = Vector256.Create((ReadOnlySpan)rows[c][1][x..].Span);
+ px.CopyTo(GetOutputRow(outputRows, c, 0)[x..]);
+ }
+
+ continue;
+ }
+
+ Vector256 vsm = Vector256.Create((ReadOnlySpan)sadMul[ix..]);
+ Vector256 inverseSigma = Vector256.Create(rowSigma[bx]) * vsm;
+
+ Vector256 xCC = Vector256.Create((ReadOnlySpan)rows[0][1 + 0][x..].Span);
+ Vector256 yCC = Vector256.Create((ReadOnlySpan)rows[1][1 + 0][x..].Span);
+ Vector256 bCC = Vector256.Create((ReadOnlySpan)rows[2][1 + 0][x..].Span);
+
+ Vector256 w = Vector256.One;
+ Vector256 X = xCC;
+ Vector256 Y = yCC;
+ Vector256 B = bCC;
+
+ // Top row
+ this.AddPixel(-1, rows, x, xCC, yCC, bCC, inverseSigma, ref X, ref Y, ref B, ref w);
+
+ // Center
+ this.AddPixel(0, rows, x - 1, xCC, yCC, bCC, inverseSigma, ref X, ref Y, ref B, ref w);
+ this.AddPixel(0, rows, x + 1, xCC, yCC, bCC, inverseSigma, ref X, ref Y, ref B, ref w);
+
+ // Bottom
+ this.AddPixel(1, rows, x, xCC, yCC, bCC, inverseSigma, ref X, ref Y, ref B, ref w);
+
+ Vector256 inverseW = Vector256.One / w;
+ (X * inverseW).CopyTo(GetOutputRow(outputRows, 0, 0)[x..]);
+ (Y * inverseW).CopyTo(GetOutputRow(outputRows, 1, 0)[x..]);
+ (B * inverseW).CopyTo(GetOutputRow(outputRows, 2, 0)[x..]);
+ }
+ }
+
+ ///
+ public override RenderPipelineChannelMode GetChannelMode(int channel)
+ {
+ if (channel < 3)
+ {
+ return RenderPipelineChannelMode.InOut;
+ }
+ else
+ {
+ return RenderPipelineChannelMode.Ignored;
+ }
+ }
+}