From 46522e28190ead2f57903837c2e326041f14ef83 Mon Sep 17 00:00:00 2001
From: winscripter <142818255+winscripter@users.noreply.github.com>
Date: Thu, 10 Sep 2026 07:24:53 +0400
Subject: [PATCH] Add progressive split
---
.../ProgressiveSplit/JxlPassDefinition.cs | 33 +++++
.../ProgressiveSplit/JxlProgressiveMode.cs | 54 ++++++++
.../JxlProgressiveSplitter.cs | 122 ++++++++++++++++++
3 files changed, 209 insertions(+)
create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/ProgressiveSplit/JxlPassDefinition.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/ProgressiveSplit/JxlProgressiveMode.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/ProgressiveSplit/JxlProgressiveSplitter.cs
diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ProgressiveSplit/JxlPassDefinition.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ProgressiveSplit/JxlPassDefinition.cs
new file mode 100644
index 0000000000..6186784ce5
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ProgressiveSplit/JxlPassDefinition.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.ProgressiveSplit;
+
+internal struct JxlPassDefinition
+{
+ public JxlPassDefinition(int numCoefficients, int shift, int suitableForDownsamplingOfAtLeast)
+ {
+ this.NumCoefficients = numCoefficients;
+ this.Shift = shift;
+ this.SuitableForDownsamplingOfAtLeast = suitableForDownsamplingOfAtLeast;
+ }
+
+ ///
+ /// Gets or sets the side of the square of the coefficients that should be kept in
+ /// each 8x8 block. Must be > 1, and at most 8. Should be in non-decreasing
+ /// order.
+ ///
+ public int NumCoefficients { get; set; }
+
+ ///
+ /// Gets or sets how much to shift the encoded values by, with rounding.
+ ///
+ public int Shift { get; set; }
+
+ ///
+ /// Gets or sets a value where, if specified indicates that if the required downsampling factor
+ /// is sufficiently high, then it is fine to stop decoding after this pass.
+ /// By default, passes are not marked as being suitable for any downsampling.
+ ///
+ public int SuitableForDownsamplingOfAtLeast { get; set; }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ProgressiveSplit/JxlProgressiveMode.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ProgressiveSplit/JxlProgressiveMode.cs
new file mode 100644
index 0000000000..e5bd18da4a
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ProgressiveSplit/JxlProgressiveMode.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.ProgressiveSplit;
+
+internal struct JxlProgressiveMode
+{
+ public int NumPasses = 1;
+ public InlineArray11 Passes;
+
+ public JxlProgressiveMode()
+ {
+ Span passesSpan = this.Passes;
+ JxlPassDefinition definitionToFill = new(numCoefficients: 8, shift: 0, suitableForDownsamplingOfAtLeast: 1);
+ passesSpan.Fill(definitionToFill);
+ }
+
+ public JxlProgressiveMode(Span passes)
+ {
+ int nump = passes.Length;
+ DebugGuard.MustBeLessThanOrEqualTo(nump, JxlShared.MaximumNumberOfPasses, nameof(nump));
+
+ this.NumPasses = nump;
+ JxlPassDefinition previousPass = new(1, 0, int.MaxValue);
+ int lastDownsamplingFactor = int.MaxValue;
+
+ for (int i = 0; i < nump; i++)
+ {
+ ref JxlPassDefinition p = ref passes[i];
+
+ if (!(p.NumCoefficients > previousPass.NumCoefficients ||
+ (p.NumCoefficients == previousPass.NumCoefficients &&
+ p.Shift < previousPass.Shift)))
+ {
+ throw new InvalidOperationException("The pass is invalid");
+ }
+
+ if (!(p.SuitableForDownsamplingOfAtLeast == int.MaxValue ||
+ p.SuitableForDownsamplingOfAtLeast <= lastDownsamplingFactor))
+ {
+ throw new InvalidOperationException("The pass is invalid");
+ }
+
+ if (p.SuitableForDownsamplingOfAtLeast != int.MaxValue)
+ {
+ lastDownsamplingFactor = p.SuitableForDownsamplingOfAtLeast;
+ }
+
+ previousPass = passes[i] = p;
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/ProgressiveSplit/JxlProgressiveSplitter.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ProgressiveSplit/JxlProgressiveSplitter.cs
new file mode 100644
index 0000000000..bc845d6970
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/ProgressiveSplit/JxlProgressiveSplitter.cs
@@ -0,0 +1,122 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy;
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.ProgressiveSplit;
+
+internal struct JxlProgressiveSplitter
+{
+ private JxlProgressiveMode progressiveMode;
+
+ public void SetProgressiveMode(JxlProgressiveMode mode) => this.progressiveMode = mode;
+
+ public readonly int GetNumPasses() => this.progressiveMode.NumPasses;
+
+ public readonly void InitializePasses(JxlPasses passes)
+ {
+ passes.NumberOfPasses = (uint)this.GetNumPasses();
+ passes.NumberOfDownsamples = 0u;
+
+ if (passes.NumberOfPasses == 0)
+ {
+ throw new InvalidOperationException("At least one pass must be present");
+ }
+
+ passes.Shift[(int)passes.NumberOfPasses - 1] = 0;
+
+ if (passes.NumberOfPasses == 1)
+ {
+ // Done. Arrays are empty.
+ return;
+ }
+
+ for (int i = 0; i < this.progressiveMode.NumPasses - 1; ++i)
+ {
+ int minDownsamplingFactor = this.progressiveMode.Passes[i].SuitableForDownsamplingOfAtLeast;
+ passes.Shift[i] = (uint)this.progressiveMode.Passes[i].Shift;
+
+ if (minDownsamplingFactor is > 1 and not int.MaxValue)
+ {
+ passes.Downsample[(int)passes.NumberOfDownsamples] = (uint)minDownsamplingFactor;
+ passes.LastPass[(int)passes.NumberOfDownsamples] = (uint)i;
+
+ if (this.progressiveMode.Passes[i + 1].SuitableForDownsamplingOfAtLeast < minDownsamplingFactor)
+ {
+ passes.NumberOfDownsamples++;
+ }
+ }
+ }
+ }
+
+ public readonly void SplitAcCoefficients(Span block, JxlAcStrategy acs, int bx, int by, Span> output)
+ where T : unmanaged, INumber, IShiftOperators
+ {
+ int size = acs.CoveredBlocksX * acs.CoveredBlocksY * JxlFrameDimensions.DctBlockSize;
+
+ if (this.progressiveMode.NumPasses == 1)
+ {
+ block.CopyTo(output[0]);
+ return;
+ }
+
+ int nCoeffsAllDoneFromEarlierPasses = 1;
+ int previousPassShift = 0;
+
+ for (int passNumber = 0; passNumber < this.progressiveMode.NumPasses; passNumber++)
+ {
+ MemoryMarshal.Cast, T>(output[passNumber..]).Slice(0, size).Clear();
+
+ int passShift = this.progressiveMode.Passes[passNumber].Shift;
+ int frameNCoeffs = this.progressiveMode.Passes[passNumber].NumCoefficients;
+ int xsize = acs.CoveredBlocksX;
+ int ysize = acs.CoveredBlocksY;
+
+ JxlForwardCoefficientOrder.CoefficientLayout(ref ysize, ref xsize);
+
+ for (int y = 0; y < ysize * frameNCoeffs; y++)
+ {
+ for (int x = 0; x < xsize * frameNCoeffs; x++)
+ {
+ int pos = (y * xsize * JxlFrameDimensions.BlockDimensions) + x;
+
+ if (x < xsize * nCoeffsAllDoneFromEarlierPasses && y < ysize * nCoeffsAllDoneFromEarlierPasses)
+ {
+ // This coefficient was already included in an earlier pass,
+ // which included a genuinely smaller set of coefficients.
+ continue;
+ }
+
+ T v = block[pos];
+
+ if (previousPassShift != 0)
+ {
+ T previousV = ShiftRightRound0(v, previousPassShift) * T.CreateSaturating(1 << previousPassShift);
+ v -= previousV;
+ }
+
+ output[passNumber][pos] = ShiftRightRound0(v, passShift);
+ }
+ }
+
+ if (this.progressiveMode.Passes[passNumber].Shift == 0)
+ {
+ nCoeffsAllDoneFromEarlierPasses = frameNCoeffs;
+ }
+
+ previousPassShift = this.progressiveMode.Passes[passNumber].Shift;
+ }
+
+ static T ShiftRightRound0(T v, int shift)
+ {
+ T oneIfNegative = v >> T.CreateSaturating(31);
+ T add = (oneIfNegative << T.CreateSaturating(shift)) - oneIfNegative;
+ return (v + add) >> T.CreateSaturating(shift);
+ }
+ }
+}