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); + } + } +}